diff --git a/ConnectionManager.iml b/ConnectionManager.iml index 945e1d0..b5ea3fe 100644 --- a/ConnectionManager.iml +++ b/ConnectionManager.iml @@ -1,6 +1,5 @@ - - + @@ -85,33 +84,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -172,6 +144,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build.xml b/build/build.xml index d9c1897..eecd43c 100644 --- a/build/build.xml +++ b/build/build.xml @@ -63,7 +63,7 @@ - + diff --git a/build/lib/dist/bouncycastle.jar b/build/lib/dist/bouncycastle.jar index ba43180..a305ba9 100644 --- a/build/lib/dist/bouncycastle.jar +++ b/build/lib/dist/bouncycastle.jar Binary files differ diff --git a/build/lib/dist/servlet.jar b/build/lib/dist/servlet.jar new file mode 100644 index 0000000..0a4cef4 --- /dev/null +++ b/build/lib/dist/servlet.jar Binary files differ diff --git a/build/lib/jetty-util.jar b/build/lib/jetty-util.jar deleted file mode 100644 index f1b2e29..0000000 --- a/build/lib/jetty-util.jar +++ /dev/null Binary files differ diff --git a/build/lib/jetty.jar b/build/lib/jetty.jar deleted file mode 100644 index 29f077d..0000000 --- a/build/lib/jetty.jar +++ /dev/null Binary files differ diff --git a/build/lib/merge/commons-lang.jar b/build/lib/merge/commons-lang.jar new file mode 100644 index 0000000..c33b353 --- /dev/null +++ b/build/lib/merge/commons-lang.jar Binary files differ diff --git a/build/lib/merge/jetty-sslengine.jar b/build/lib/merge/jetty-sslengine.jar new file mode 100644 index 0000000..b7404ea --- /dev/null +++ b/build/lib/merge/jetty-sslengine.jar Binary files differ diff --git a/build/lib/merge/jetty-util.jar b/build/lib/merge/jetty-util.jar new file mode 100644 index 0000000..a003573 --- /dev/null +++ b/build/lib/merge/jetty-util.jar Binary files differ diff --git a/build/lib/merge/jetty.jar b/build/lib/merge/jetty.jar new file mode 100644 index 0000000..2622779 --- /dev/null +++ b/build/lib/merge/jetty.jar Binary files differ diff --git a/build/lib/merge/mina-core.jar b/build/lib/merge/mina-core.jar index e49b017..b984bad 100644 --- a/build/lib/merge/mina-core.jar +++ b/build/lib/merge/mina-core.jar Binary files differ diff --git a/build/lib/merge/mina-filter-compression.jar b/build/lib/merge/mina-filter-compression.jar index a67ff54..13cfa1a 100644 --- a/build/lib/merge/mina-filter-compression.jar +++ b/build/lib/merge/mina-filter-compression.jar Binary files differ diff --git a/build/lib/merge/mina-filter-ssl.jar b/build/lib/merge/mina-filter-ssl.jar index 4df959a..a162ceb 100644 --- a/build/lib/merge/mina-filter-ssl.jar +++ b/build/lib/merge/mina-filter-ssl.jar Binary files differ diff --git a/build/lib/servlet-api.jar b/build/lib/servlet-api.jar deleted file mode 100644 index a604b6f..0000000 --- a/build/lib/servlet-api.jar +++ /dev/null Binary files differ diff --git a/build/lib/versions.txt b/build/lib/versions.txt index 944a7e7..f3bc68a 100644 --- a/build/lib/versions.txt +++ b/build/lib/versions.txt @@ -3,19 +3,21 @@ ant.jar | Jetty 5.1.10 ant-contrib.jar | 1.0b1 ant-subdirtask.jar | Revision 1.4 (CVS) -bouncycastle.jar | JDK 1.5, 138 (bcprov-jdk15-138.jar) -commons-el.jar | Jetty 5.1.10 +bouncycastle.jar | JDK 1.5, 139 (bcprov-jdk15-139.jar) +commons-el.jar | Jetty 6.0.1 (1.0) +commons-lang.jar | 2.3 dom4j.jar | 1.6.1 !jaxen.jar | 1.1 beta 4 (from DOM4J 1.6.1) -jetty.jar | 6.0.1 -jetty-util.jar | 6.0.1 +jetty.jar | Jetty 6.1.10 +jetty-sslengine.jar | Jetty 6.1.10 +jetty-util.jar | Jetty 6.1.10 junit.jar | 3.8.1 jdic.jar | 0.9.1 (for windows only) jzlib.jar | 1.0.7 -mina-core.jar | 1.1.6 (https://svn.apache.org/repos/asf/mina/branches/1.1) -mina-filter-compression.jar | 1.1.6 (https://svn.apache.org/repos/asf/mina/branches/1.1) -mina-filter-ssl.jar | 1.1.6 (https://svn.apache.org/repos/asf/mina/branches/1.1) +mina-core.jar | 1.1.7 (https://svn.apache.org/repos/asf/mina/branches/1.1) +mina-filter-compression.jar | 1.1.7 (https://svn.apache.org/repos/asf/mina/branches/1.1) +mina-filter-ssl.jar | 1.1.7 (https://svn.apache.org/repos/asf/mina/branches/1.1) pack200task.jar | August 5, 2004 -servlet-api.jar | 2.5-6.0.1 +servlet.jar | Jetty 6.1.10 (2.5) xmltask.jar | 1.11 xpp3.jar | XPP_3 1.1.4c \ No newline at end of file diff --git a/src/conf/manager.xml b/src/conf/manager.xml index 5c7c397..48041cb 100644 --- a/src/conf/manager.xml +++ b/src/conf/manager.xml @@ -101,8 +101,29 @@ 16 --> + false + + 7070 + 7443 + + + + false + + + + + + + 5 + + 2 + + + 30 + diff --git a/src/java/org/dom4j/io/XMPPPacketReader.java b/src/java/org/dom4j/io/XMPPPacketReader.java index 31e381f..90c0f8a 100644 --- a/src/java/org/dom4j/io/XMPPPacketReader.java +++ b/src/java/org/dom4j/io/XMPPPacketReader.java @@ -129,6 +129,20 @@ } /** + *

Reads a Document from the given stream

+ * + * @param charSet the charSet that the input is encoded in + * @param in InputStream to read from. + * @return the newly created Document instance + * @throws DocumentException if an error occurs during parsing. + */ + public Document read(String charSet, InputStream in) + throws DocumentException, IOException, XmlPullParserException + { + return read(createReader(in, charSet)); + } + + /** *

Reads a Document from the given Reader

* * @param reader is the reader for the input @@ -273,6 +287,23 @@ return lastActive > lastHeartbeat ? lastActive : lastHeartbeat; } + /* + * DANIELE: Add parse document by string + */ + public Document parseDocument(String xml) throws DocumentException { + /* + // Long way with reuse of DocumentFactory. + DocumentFactory df = getDocumentFactory(); + SAXReader reader = new SAXReader( df ); + Document document = reader.read( new StringReader( xml );*/ + + // Simple way + // TODO Optimize. Do not create a sax reader for each parsing + Document document = DocumentHelper.parseText(xml); + + return document; + } + // Implementation methods //------------------------------------------------------------------------- public Document parseDocument() throws DocumentException, IOException, XmlPullParserException { @@ -417,6 +448,10 @@ protected Reader createReader(InputStream in) throws IOException { return new BufferedReader(new InputStreamReader(in)); } + + private Reader createReader(InputStream in, String charSet) throws UnsupportedEncodingException { + return new BufferedReader(new InputStreamReader(in, charSet)); + } } /* diff --git a/src/java/org/jivesoftware/multiplexer/ClientSession.java b/src/java/org/jivesoftware/multiplexer/ClientSession.java index a7354b8..1408659 100644 --- a/src/java/org/jivesoftware/multiplexer/ClientSession.java +++ b/src/java/org/jivesoftware/multiplexer/ClientSession.java @@ -18,6 +18,9 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.net.InetAddress; +import java.net.UnknownHostException; + /** * Session that represents a client to server connection. * @@ -121,7 +124,13 @@ // Register that the new session is associated with the specified stream ID Session.addSession(streamID, session); // Send to the server that a new client session has been created - serverSurrogate.clientSessionCreated(streamID); + InetAddress address = null; + try { + address = connection.getInetAddress(); + } catch (UnknownHostException e) { + // Do nothing + } + serverSurrogate.clientSessionCreated(streamID, address); // Build the start packet response StringBuilder sb = new StringBuilder(200); diff --git a/src/java/org/jivesoftware/multiplexer/ConnectionManager.java b/src/java/org/jivesoftware/multiplexer/ConnectionManager.java index 04b5dd2..0823735 100644 --- a/src/java/org/jivesoftware/multiplexer/ConnectionManager.java +++ b/src/java/org/jivesoftware/multiplexer/ConnectionManager.java @@ -120,7 +120,6 @@ private ServerSurrogate serverSurrogate; private SocketAcceptor socketAcceptor; private SocketAcceptor sslSocketAcceptor; - private HttpBindManager httpBindManager; /** * Returns a singleton instance of ConnectionManager. @@ -148,7 +147,7 @@ name = JiveGlobals.getXMLProperty("xmpp.manager.name", StringUtils.randomString(5)).toLowerCase(); serverName = JiveGlobals.getXMLProperty("xmpp.domain"); - version = new Version(3, 5, 0, Version.ReleaseStatus.Release, -1); + version = new Version(3, 5, 2, Version.ReleaseStatus.Release, -1); if (serverName != null) { setupMode = false; } @@ -277,10 +276,7 @@ } } - private void startClientListeners(String localIPAddress) { - // TODO Does MINA uses MAX_PRIORITY for threads? - // TODO Are threads running as daemon? Can we stop de server? - // Start clients plain socket unless it's been disabled. + public int getClientListenerPort() { int port = 5222; // Check if old property is being used for storing c2s port if (JiveGlobals.getXMLProperty("xmpp.socket.plain.port") != null) { @@ -290,6 +286,14 @@ else if (JiveGlobals.getXMLProperty("xmpp.socket.default.port") != null) { port = JiveGlobals.getIntProperty("xmpp.socket.default.port", 5222); } + return port; + } + + private void startClientListeners(String localIPAddress) { + // TODO Does MINA uses MAX_PRIORITY for threads? + // TODO Are threads running as daemon? Can we stop de server? + // Start clients plain socket unless it's been disabled. + int port = getClientListenerPort(); // Create SocketAcceptor with correct number of processors socketAcceptor = buildSocketAcceptor(); // Customize Executor that will be used by processors to process incoming stanzas @@ -419,32 +423,20 @@ return; } - int plainPort = JiveGlobals.getIntProperty("xmpp.httpbind.port.plain", 8080); - int sslPort = JiveGlobals.getIntProperty("xmpp.httpbind.port.secure", 8443); - httpBindManager = new HttpBindManager(serverName, plainPort, sslPort); - try { - httpBindManager.startup(); + HttpBindManager.getInstance().start(); } catch (Exception e) { - httpBindManager = null; - System.err.println("Error starting http bind servlet " + plainPort + " and " + sslPort - + ": " + e.getMessage()); Log.error(LocaleUtils.getLocalizedString("admin.error.http.bind"), e); } } private void stopHttpBindServlet() { - if (httpBindManager != null) { - try { - httpBindManager.shutdown(); - } - catch (Exception e) { - Log.error(e); - } - finally { - httpBindManager = null; - } + try { + HttpBindManager.getInstance().stop(); + } + catch (Exception e) { + Log.error(e); } } diff --git a/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java b/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java index db3aa30..b9d5aed 100644 --- a/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java +++ b/src/java/org/jivesoftware/multiplexer/ConnectionWorkerThread.java @@ -29,6 +29,7 @@ import java.io.InputStreamReader; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.InetAddress; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Iterator; @@ -384,14 +385,16 @@ * Sends a notification to the main server that a new client session has been created. * * @param streamID the stream ID assigned by the connection manager to the new session. + * @param address the remote address of the client. */ - public void clientSessionCreated(String streamID) { + public void clientSessionCreated(String streamID, InetAddress address) { StringBuilder sb = new StringBuilder(100); sb.append(""); + sb.append("'>"); // Forward the notification to the server connection.deliver(sb.toString()); } diff --git a/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java b/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java index b015b05..527927b 100644 --- a/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java +++ b/src/java/org/jivesoftware/multiplexer/ServerPacketHandler.java @@ -89,8 +89,32 @@ if (Log.isDebugEnabled()) { Log.debug("IQ stanza of type RESULT was discarded: " + stanza.asXML()); } + } else if ("error".equals(type)) { + // Close session if child element is CREATE + Element wrapper = stanza.element("session"); + if (wrapper != null) { + String streamID = wrapper.attributeValue("id"); + // Check if the server is informing us that we need to close a session + if (wrapper.element("create") != null) { + // Get the session that matches the requested stream ID + Session session = Session.getSession(streamID); + if (session != null) { + session.close(); + } + } else { + if (Log.isDebugEnabled()) { + Log.debug("IQ stanza of type ERRROR was discarded: " + stanza.asXML()); + } + } + } else { + if (Log.isDebugEnabled()) { + Log.debug("IQ stanza of type ERRROR was discarded: " + stanza.asXML()); + } + } } else { - Log.warn("IQ stanza with invalid type was discarded: " + stanza.asXML()); + if (Log.isDebugEnabled()) { + Log.debug("IQ stanza with invalid type was discarded: " + stanza.asXML()); + } } } else if ("error".equals(tag) && "stream".equals(stanza.getNamespacePrefix())) { if (stanza.element("system-shutdown") != null) { diff --git a/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java b/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java index 45df35d..84a415f 100644 --- a/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java +++ b/src/java/org/jivesoftware/multiplexer/ServerSurrogate.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import java.net.InetAddress; /** * Surrogate of the main server where the Connection Manager is routing client @@ -145,9 +146,10 @@ * a notification to the main server. * * @param streamID the stream ID assigned by the connection manager to the new session. + * @param address the remote address of the connection. */ - public void clientSessionCreated(final String streamID) { - threadPool.execute(new NewSessionTask(streamID)); + public void clientSessionCreated(final String streamID, final InetAddress address) { + threadPool.execute(new NewSessionTask(streamID, address)); } /** diff --git a/src/java/org/jivesoftware/multiplexer/net/NIOConnection.java b/src/java/org/jivesoftware/multiplexer/net/NIOConnection.java index fb516c6..47dbc6c 100644 --- a/src/java/org/jivesoftware/multiplexer/net/NIOConnection.java +++ b/src/java/org/jivesoftware/multiplexer/net/NIOConnection.java @@ -197,7 +197,7 @@ if (session == null) { return !ioSession.isConnected(); } - return session.getStatus() == Session.STATUS_CLOSED; + return session.getStatus() == Session.STATUS_CLOSED && !ioSession.isConnected(); } public boolean isSecure() { diff --git a/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java b/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java index 0a1eb0b..3e5c421 100644 --- a/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java +++ b/src/java/org/jivesoftware/multiplexer/net/SSLConfig.java @@ -14,6 +14,9 @@ import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.KeyManagerFactory; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -39,6 +42,7 @@ private static String keyStoreLocation; private static String trustStoreLocation; private static String storeType; + private static SSLContext context; private SSLConfig() { } @@ -74,6 +78,16 @@ sslFactory = (SSLJiveServerSocketFactory)SSLJiveServerSocketFactory.getInstance( algorithm, keyStore, trustStore); + + context = SSLContext.getInstance(algorithm); + + KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyFactory.init(keyStore, SSLConfig.getKeyPassword().toCharArray()); + TrustManagerFactory c2sTrustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + c2sTrustFactory.init(trustStore); + context.init(keyFactory.getKeyManagers(), + c2sTrustFactory.getTrustManagers(), + new java.security.SecureRandom()); } catch (Exception e) { Log.error("SSLConfig startup problem.\n" + @@ -132,6 +146,15 @@ return trustStore; } + /** + * Get the SSLContext for c2s connections + * + * @return the SSLContext for c2s connections + */ + public static SSLContext getSSLContext() { + return context; + } + public static void saveStores() throws IOException { try { keyStore.store(new FileOutputStream(keyStoreLocation), keypass.toCharArray()); diff --git a/src/java/org/jivesoftware/multiplexer/net/XMLLightweightParser.java b/src/java/org/jivesoftware/multiplexer/net/XMLLightweightParser.java index 54ae0b7..e8865de 100644 --- a/src/java/org/jivesoftware/multiplexer/net/XMLLightweightParser.java +++ b/src/java/org/jivesoftware/multiplexer/net/XMLLightweightParser.java @@ -11,12 +11,16 @@ package org.jivesoftware.multiplexer.net; import org.apache.mina.common.ByteBuffer; +import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; +import org.jivesoftware.util.PropertyEventDispatcher; +import org.jivesoftware.util.PropertyEventListener; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * This is a Light-Weight XML Parser. @@ -30,6 +34,8 @@ * @author Gaston Dombiak */ class XMLLightweightParser { + private static final String MAX_PROPERTY_NAME = "xmpp.parser.buffer.size"; + private static int maxBufferSize; // Chars that rappresent CDATA section start protected static char[] CDATA_START = {'<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['}; // Chars that rappresent CDATA section end @@ -84,6 +90,13 @@ Charset encoder; + static { + // Set default max buffer size to 1MB. If limit is reached then close connection + maxBufferSize = JiveGlobals.getIntProperty(MAX_PROPERTY_NAME, 1048576); + // Listen for changes to this property + PropertyEventDispatcher.addListener(new PropertyListener()); + } + public XMLLightweightParser(String charset) { encoder = Charset.forName(charset); } @@ -147,13 +160,18 @@ invalidateBuffer(); // Check that the buffer is not bigger than 1 Megabyte. For security reasons // we will abort parsing when 1 Mega of queued chars was found. - if (buffer.length() > 1048576) { + if (buffer.length() > maxBufferSize) { throw new Exception("Stopped parsing never ending stanza"); } CharBuffer charBuffer = encoder.decode(byteBuffer.buf()); char[] buf = charBuffer.array(); int readByte = charBuffer.remaining(); + // Just return if nothing was read + if (readByte == 0) { + return; + } + // Verify if the last received byte is an incomplete double byte character char lastChar = buf[readByte-1]; if (lastChar >= 0xfff0) { @@ -183,8 +201,26 @@ } // Robot. char ch; + boolean isHighSurrogate = false; for (int i = 0; i < readByte; i++) { ch = buf[i]; + if (isHighSurrogate) { + if (Character.isLowSurrogate(ch)) { + // Everything is fine. Clean up traces for surrogates + isHighSurrogate = false; + } + else { + // Trigger error. Found high surrogate not followed by low surrogate + throw new Exception("Found high surrogate not followed by low surrogate"); + } + } + else if (Character.isHighSurrogate(ch)) { + isHighSurrogate = true; + } + else if (Character.isLowSurrogate(ch)) { + // Trigger error. Found low surrogate char without a preceding high surrogate + throw new Exception("Found low surrogate char without a preceding high surrogate"); + } if (status == XMLLightweightParser.TAIL) { // Looking for the close tag if (depth < 1 && ch == head.charAt(tailCount)) { @@ -330,4 +366,30 @@ foundMsg(""); } } + + private static class PropertyListener implements PropertyEventListener { + public void propertySet(String property, Map params) { + if (MAX_PROPERTY_NAME.equals(property)) { + String value = (String) params.get("value"); + if (value != null) { + maxBufferSize = Integer.parseInt(value); + } + } + } + + public void propertyDeleted(String property, Map params) { + if (MAX_PROPERTY_NAME.equals(property)) { + // Use default value when none was specified + maxBufferSize = 1048576; + } + } + + public void xmlPropertySet(String property, Map params) { + // Do nothing + } + + public void xmlPropertyDeleted(String property, Map params) { + // Do nothing + } + } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java b/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java new file mode 100644 index 0000000..38aa47d --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java @@ -0,0 +1,158 @@ +/** + * $Revision:$ + * $Date:$ + * + * Copyright (C) 2005-2008 Jive Software. All rights reserved. + * This software is the proprietary information of Jive Software. Use is subject to license terms. + */ +package org.jivesoftware.multiplexer.net.http; + +import javax.servlet.http.HttpServletResponse; + +/** + * An enum defining all errors which can happen during a BOSH session. + */ +public enum BoshBindingError { + /** + * The format of an HTTP header or binding element received from the client is unacceptable + * (e.g., syntax error), or Script Syntax is not supported. + */ + badRequest(Type.terminal, "bad-request", HttpServletResponse.SC_BAD_REQUEST), + /** + * The target domain specified in the 'to' attribute or the target host or port specified in the + * 'route' attribute is no longer serviced by the connection manager. + */ + hostGone(Type.terminal, "host-gone"), + /** + * The target domain specified in the 'to' attribute or the target host or port specified in the + * 'route' attribute is unknown to the connection manager. + */ + hostUnknown(Type.terminal, "host-unknown"), + /** + * The initialization element lacks a 'to' or 'route' attribute (or the attribute has no value) + * but the connection manager requires one. + */ + improperAddressing(Type.terminal, "improper-addressing"), + /** + * The connection manager has experienced an internal error that prevents it from servicing the + * request. + */ + internalServerError(Type.terminal, "internal-server-error"), + /** + * (1) 'sid' is not valid, (2) 'stream' is not valid, (3) 'rid' is larger than the upper limit + * of the expected window, (4) connection manager is unable to resend response, (5) 'key' + * sequence is invalid + */ + itemNotFound(Type.terminal, "item-not-found", HttpServletResponse.SC_NOT_FOUND), + /** + * Another request being processed at the same time as this request caused the session to + * terminate. + */ + otherRequest(Type.terminal, "other-request"), + /** + * The client has broken the session rules (polling too frequently, requesting too frequently, + * too many simultaneous requests). + */ + policyViolation(Type.terminal, "policy-violation", + HttpServletResponse.SC_FORBIDDEN), + /** + * The connection manager was unable to connect to, or unable to connect securely to, or has + * lost its connection to, the server. + */ + remoteConnectionFailed(Type.terminal, "remote-connection-failed"), + /** + * Encapsulates an error in the protocol being transported. + */ + remoteStreamError(Type.terminal, "remote-stream-error"), + /** + * The connection manager does not operate at this URI (e.g., the connection manager accepts + * only SSL or TLS connections at some https: URI rather than the http: URI requested by the + * client). The client may try POSTing to the URI in the content of the <uri/> child + * element. + */ + seeOtherUri(Type.terminal, "see-other-uri"), + /** + * The connection manager is being shut down. All active HTTP sessions are being terminated. No + * new sessions can be created. + */ + systemShutdown(Type.terminal, "system-shutdown"), + /** + * The error is not one of those defined herein; the connection manager SHOULD include + * application-specific information in the content of the <body> wrapper. + */ + undefinedCondition(Type.terminal, "undefined-condition"); + + private Type errorType; + private String condition; + private int legacyErrorCode = HttpServletResponse.SC_BAD_REQUEST; + + BoshBindingError(Type errorType, String condition, int legacyErrorCode) { + this(errorType, condition); + this.legacyErrorCode = legacyErrorCode; + } + + BoshBindingError(Type errorType, String condition) { + this.errorType = errorType; + this.condition = condition; + } + + public Type getErrorType() { + return errorType; + } + + /** + * Returns the condition that caused the binding error. This should be returned to the client + * so that the client can take appropriate action. + * + * @return the condition that caused the binding error. + */ + public String getCondition() { + return condition; + } + + /** + * Returns the legacy HTTP error code which is related to the binding error. With the 1.6 + * version of BOSH the use of HTTP errors was deprecated in favor of using errors inside of the + * response to the client so that they could be more easily processed on the client side. + * + * @return the legacy HTTP error code which is related to the binding error. + */ + public int getLegacyErrorCode() { + return legacyErrorCode; + } + + public enum Type { + /** + * The terminal error condition prevents the client from making any further requests until a + * new session is established. + */ + terminal(null), + /** + * In the case of a recoverable binding error the client MUST repeat the HTTP request and + * all the preceding HTTP requests that have not received responses. The content of these + * requests MUST be identical to the <body> elements of the original requests. This + * allows the connection manager to recover a session after the previous request was lost + * due to a communication failure. + */ + recoverable("error"); + private String type; + + Type(String type) { + this.type = type; + } + + /** + * Returns the type that will be displayed to the client. + * + * @return the type that will be displayed to the client. + */ + public String getType() { + if (type == null) { + return name(); + } + else { + return type; + } + } + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java b/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java new file mode 100644 index 0000000..7b42660 --- /dev/null +++ b/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java @@ -0,0 +1,49 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright (C) 2005-2008 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. + */ +package org.jivesoftware.multiplexer.net.http; + +import org.jivesoftware.multiplexer.ConnectionManager; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.ServletException; +import java.io.IOException; + +/** + * Serves up the flash cross domain xml file which allows other domains to access http-binding + * using flash. + * + * @author Alexander Wenckus + */ +public class FlashCrossDomainServlet extends HttpServlet { + + public static String CROSS_DOMAIN_TEXT = "" + + "" + + "" + + ""; + + @Override + protected void doGet(HttpServletRequest httpServletRequest, + HttpServletResponse response) throws + ServletException, IOException { + StringBuilder builder = new StringBuilder(); + builder.append(CROSS_DOMAIN_TEXT + + ConnectionManager.getInstance().getClientListenerPort() + + CROSS_DOMAIN_END_TEXT); + builder.append("\n"); + response.setContentType("text/xml"); + response.getOutputStream().write(builder.toString().getBytes()); + } +} diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java index f2e6f06..33a240d 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java @@ -1,31 +1,33 @@ /** - * $RCSfile: $ - * $Revision: $ - * $Date: $ + * $RCSfile$ + * $Revision: $ + * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. - * This software is the proprietary information of Jive Software. Use is subject to license terms. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; /** * */ public class HttpBindException extends Exception { - private boolean shouldCloseSession; - private int httpError; + private BoshBindingError error; - public HttpBindException(String message, boolean shouldCloseSession, int httpError) { + public HttpBindException(String message, BoshBindingError error) { super(message); - this.shouldCloseSession = shouldCloseSession; - this.httpError = httpError; + this.error = error; } - public int getHttpError() { - return httpError; + public BoshBindingError getBindingError() { + return error; } public boolean shouldCloseSession() { - return shouldCloseSession; + return error.getErrorType() == BoshBindingError.Type.terminal; } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindManager.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindManager.java index e7c503f..02b7c5a 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindManager.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindManager.java @@ -3,110 +3,500 @@ * $Revision: $ * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), - * a copy of which is included in this distribution. + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; -import org.mortbay.jetty.Server; +import org.jivesoftware.util.*; +import org.jivesoftware.multiplexer.net.SSLConfig; +import org.jivesoftware.multiplexer.ConnectionManager; import org.mortbay.jetty.Connector; import org.mortbay.jetty.Handler; -import org.mortbay.jetty.security.SslSocketConnector; -import org.mortbay.jetty.servlet.ServletHolder; -import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.handler.ContextHandler; +import org.mortbay.jetty.handler.ContextHandlerCollection; +import org.mortbay.jetty.handler.DefaultHandler; import org.mortbay.jetty.nio.SelectChannelConnector; -import org.jivesoftware.multiplexer.net.SSLConfig; -import org.jivesoftware.util.Log; - -import javax.net.ssl.SSLServerSocketFactory; +import org.mortbay.jetty.security.SslSelectChannelConnector; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.jetty.webapp.WebAppContext; +import javax.net.ssl.SSLContext; +import java.io.File; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; /** - * Manages connections to the server which use the HTTP Bind protocol specified in XEP-0124. The manager maps a servlet to - * an embedded servlet container using the ports provided in the constructor. * - * @author Alexander Wenckus */ -public class HttpBindManager { - private int plainPort; - private int sslPort; - private Server server; - private String serverName; +public final class HttpBindManager { - public HttpBindManager(String serverName, int plainPort, int sslPort) { - this.plainPort = plainPort; - this.sslPort = sslPort; - this.server = new Server(); - this.serverName = serverName; + public static final String HTTP_BIND_ENABLED = "xmpp.httpbind.enabled"; + + public static final boolean HTTP_BIND_ENABLED_DEFAULT = true; + + public static final String HTTP_BIND_PORT = "xmpp.httpbind.port.plain"; + + public static final int HTTP_BIND_PORT_DEFAULT = 7070; + + public static final String HTTP_BIND_SECURE_PORT = "xmpp.httpbind.port.secure"; + + public static final int HTTP_BIND_SECURE_PORT_DEFAULT = 7443; + + private static HttpBindManager instance = new HttpBindManager(); + + private Server httpBindServer; + + private int bindPort; + + private int bindSecurePort; + + private CertificateListener certificateListener; + + private HttpSessionManager httpSessionManager; + + private ContextHandlerCollection contexts; + + public static HttpBindManager getInstance() { + return instance; } - /** - * Starts the HTTP Bind service. - * - * @throws Exception if there is an error starting up the server. - */ - public void startup() throws Exception { - for(Connector connector : createConnectors()) { - server.addConnector(connector); + private HttpBindManager() { + // Configure Jetty logging to a more reasonable default. + System.setProperty("org.mortbay.log.class", "org.jivesoftware.util.log.util.JettyLog"); + // JSP 2.0 uses commons-logging, so also override that implementation. + System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory"); + + PropertyEventDispatcher.addListener(new HttpServerPropertyListener()); + this.httpSessionManager = new HttpSessionManager(); + contexts = new ContextHandlerCollection(); + } + + public void start() { + certificateListener = new CertificateListener(); + CertificateManager.addListener(certificateListener); + + if (!isHttpBindServiceEnabled()) { + return; } - server.addHandler(createServletHandler()); + bindPort = getHttpBindUnsecurePort(); + bindSecurePort = getHttpBindSecurePort(); + configureHttpBindServer(bindPort, bindSecurePort); - server.start(); + try { + httpBindServer.start(); + } + catch (Exception e) { + Log.error("Error starting HTTP bind service", e); + } } - private Handler createServletHandler() { - ServletHolder servletHolder = new ServletHolder( - new HttpBindServlet(new HttpSessionManager(serverName))); - ServletHandler servletHandler = new ServletHandler(); - servletHandler.addServletWithMapping(servletHolder, "/"); - return servletHandler; - } + public void stop() { + CertificateManager.removeListener(certificateListener); - private Connector[] createConnectors() { - SelectChannelConnector connector = new SelectChannelConnector(); - connector.setPort(plainPort); - - if (sslPort > 0) { + if (httpBindServer != null) { try { - SslSocketConnector secureConnector = new JiveSslConnector(); - secureConnector.setPort(sslPort); - - secureConnector.setTrustPassword(SSLConfig.getTrustPassword()); - secureConnector.setTruststoreType(SSLConfig.getStoreType()); - secureConnector.setTruststore(SSLConfig.getTruststoreLocation()); - secureConnector.setNeedClientAuth(false); - secureConnector.setWantClientAuth(false); - - secureConnector.setKeyPassword(SSLConfig.getKeyPassword()); - secureConnector.setKeystoreType(SSLConfig.getStoreType()); - secureConnector.setKeystore(SSLConfig.getKeystoreLocation()); - - return new Connector[]{connector, secureConnector}; + httpBindServer.stop(); } - catch (Exception ex) { - Log.error("Error establishing SSL connector for HTTP Bind", ex); + catch (Exception e) { + Log.error("Error stoping HTTP bind service", e); } } + } - return new Connector[]{connector}; + public HttpSessionManager getSessionManager() { + return httpSessionManager; + } + + private boolean isHttpBindServiceEnabled() { + return JiveGlobals.getBooleanProperty(HTTP_BIND_ENABLED, HTTP_BIND_ENABLED_DEFAULT); + } + + private Connector createConnector(int port) { + if (port > 0) { + SelectChannelConnector connector = new SelectChannelConnector(); + // Listen on a specific network interface if it has been set. + connector.setHost(getBindInterface()); + connector.setPort(port); + return connector; + } + return null; + } + + private Connector createSSLConnector(int securePort) { + try { + if (securePort > 0 && CertificateManager.isRSACertificate(SSLConfig.getKeyStore(), "*")) { + if (!CertificateManager.isRSACertificate(SSLConfig.getKeyStore(), + ConnectionManager.getInstance().getServerName())) { + Log.warn("HTTP binding: Using RSA certificates but they are not valid for " + + "the hosted domain"); + } + + JiveSslConnector sslConnector = new JiveSslConnector(); + sslConnector.setHost(getBindInterface()); + sslConnector.setPort(securePort); + + sslConnector.setTrustPassword(SSLConfig.getTrustPassword()); + sslConnector.setTruststoreType(SSLConfig.getStoreType()); + sslConnector.setTruststore(SSLConfig.getTruststoreLocation()); + sslConnector.setNeedClientAuth(false); + sslConnector.setWantClientAuth(false); + + sslConnector.setKeyPassword(SSLConfig.getKeyPassword()); + sslConnector.setKeystoreType(SSLConfig.getStoreType()); + sslConnector.setKeystore(SSLConfig.getKeystoreLocation()); + return sslConnector; + } + } + catch (Exception e) { + Log.error("Error creating SSL connector for Http bind", e); + } + return null; + } + + private String getBindInterface() { + String interfaceName = JiveGlobals.getXMLProperty("network.interface"); + String bindInterface = null; + if (interfaceName != null) { + if (interfaceName.trim().length() > 0) { + bindInterface = interfaceName; + } + } + return bindInterface; } /** - * Shutdown the HTTP Bind service, freeing any related resources. + * Returns true if the HTTP binding server is currently enabled. * - * @throws Exception if there is an error shutting down the service. + * @return true if the HTTP binding server is currently enabled. */ - public void shutdown() throws Exception { - server.stop(); + public boolean isHttpBindEnabled() { + return httpBindServer != null && httpBindServer.isRunning(); } - private class JiveSslConnector extends SslSocketConnector { + public String getHttpBindUnsecureAddress() { + return "http://" + ConnectionManager.getInstance().getServerName() + ":" + + bindPort + "/http-bind/"; + } + + public String getHttpBindSecureAddress() { + return "https://" + ConnectionManager.getInstance().getServerName() + ":" + + bindSecurePort + "/http-bind/"; + } + + public String getJavaScriptUrl() { + return "http://" + ConnectionManager.getInstance().getServerName() + ":" + + bindPort + "/scripts/"; + } + + public void setHttpBindEnabled(boolean isEnabled) { + JiveGlobals.setXMLProperty(HTTP_BIND_ENABLED, String.valueOf(isEnabled)); + } + + /** + * Set the ports on which the HTTP binding service will be running. + * + * @param unsecurePort the unsecured connection port which clients can connect to. + * @param securePort the secured connection port which clients can connect to. + * @throws Exception when there is an error configuring the HTTP binding ports. + */ + public void setHttpBindPorts(int unsecurePort, int securePort) throws Exception { + changeHttpBindPorts(unsecurePort, securePort); + bindPort = unsecurePort; + bindSecurePort = securePort; + if (unsecurePort != HTTP_BIND_PORT_DEFAULT) { + JiveGlobals.setXMLProperty(HTTP_BIND_PORT, String.valueOf(unsecurePort)); + } + else { + JiveGlobals.deleteXMLProperty(HTTP_BIND_PORT); + } + if (securePort != HTTP_BIND_SECURE_PORT_DEFAULT) { + JiveGlobals.setXMLProperty(HTTP_BIND_SECURE_PORT, String.valueOf(securePort)); + } + else { + JiveGlobals.deleteXMLProperty(HTTP_BIND_SECURE_PORT); + } + } + + private synchronized void changeHttpBindPorts(int unsecurePort, int securePort) + throws Exception { + if (unsecurePort < 0 && securePort < 0) { + throw new IllegalArgumentException("At least one port must be greater than zero."); + } + if (unsecurePort == securePort) { + throw new IllegalArgumentException("Ports must be distinct."); + } + + if (httpBindServer != null) { + try { + httpBindServer.stop(); + } + catch (Exception e) { + Log.error("Error stopping http bind server", e); + } + } + + configureHttpBindServer(unsecurePort, securePort); + httpBindServer.start(); + } + + /** + * Starts an HTTP Bind server on the specified port and secure port. + * + * @param port the port to start the normal (unsecured) HTTP Bind service on. + * @param securePort the port to start the TLS (secure) HTTP Bind service on. + */ + private synchronized void configureHttpBindServer(int port, int securePort) { + httpBindServer = new Server(); + Connector httpConnector = createConnector(port); + Connector httpsConnector = createSSLConnector(securePort); + if (httpConnector == null && httpsConnector == null) { + httpBindServer = null; + return; + } + if (httpConnector != null) { + httpBindServer.addConnector(httpConnector); + } + if (httpsConnector != null) { + httpBindServer.addConnector(httpsConnector); + } + + createBoshHandler(contexts, "/http-bind"); + createCrossDomainHandler(contexts, "/"); + loadStaticDirectory(contexts); + + httpBindServer.setHandlers(new Handler[]{contexts, new DefaultHandler()}); + } + + private void createBoshHandler(ContextHandlerCollection contexts, String boshPath) { + ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(HttpBindServlet.class, "/"); + + ContextHandler boshContextHandler = new ContextHandler(contexts, boshPath); + boshContextHandler.setHandler(handler); + } + + private void createCrossDomainHandler(ContextHandlerCollection contexts, String crossPath) { + ServletHandler handler = new ServletHandler(); + handler.addServletWithMapping(FlashCrossDomainServlet.class, "/crossdomain.xml"); + + ContextHandler crossContextHandler = new ContextHandler(contexts, crossPath); + crossContextHandler.setHandler(handler); + } + + private void loadStaticDirectory(ContextHandlerCollection contexts) { + File spankDirectory = new File(JiveGlobals.getHomeDirectory() + File.separator + + "resources" + File.separator + "spank"); + if (spankDirectory.exists()) { + if (spankDirectory.canRead()) { + WebAppContext context = new WebAppContext(contexts, spankDirectory.getPath(), "/"); + context.setWelcomeFiles(new String[]{"index.html"}); + } + else { + Log.warn("Openfire cannot read the directory: " + spankDirectory); + } + } + } + + public ContextHandlerCollection getContexts() { + return contexts; + } + + private void doEnableHttpBind(boolean shouldEnable) { + if (shouldEnable && httpBindServer == null) { + try { + changeHttpBindPorts(JiveGlobals.getIntProperty(HTTP_BIND_PORT, + HTTP_BIND_PORT_DEFAULT), JiveGlobals.getIntProperty(HTTP_BIND_SECURE_PORT, + HTTP_BIND_SECURE_PORT_DEFAULT)); + } + catch (Exception e) { + Log.error("Error configuring HTTP binding ports", e); + } + } + else if (!shouldEnable && httpBindServer != null) { + try { + httpBindServer.stop(); + } + catch (Exception e) { + Log.error("Error stopping HTTP bind service", e); + } + httpBindServer = null; + } + } + + /** + * Returns the HTTP binding port which does not use SSL. + * + * @return the HTTP binding port which does not use SSL. + */ + public int getHttpBindUnsecurePort() { + return JiveGlobals.getIntProperty(HTTP_BIND_PORT, HTTP_BIND_PORT_DEFAULT); + } + + /** + * Returns the HTTP binding port which uses SSL. + * + * @return the HTTP binding port which uses SSL. + */ + public int getHttpBindSecurePort() { + return JiveGlobals.getIntProperty(HTTP_BIND_SECURE_PORT, HTTP_BIND_SECURE_PORT_DEFAULT); + } + + /** + * Returns true if script syntax is enabled. Script syntax allows BOSH to be used in + * environments where clients may be restricted to using a particular server. Instead of using + * standard HTTP Post requests to transmit data, HTTP Get requests are used. + * + * @return true if script syntax is enabled. + * @see BOSH: Alternative Script + * Syntax + */ + public boolean isScriptSyntaxEnabled() { + return JiveGlobals.getBooleanProperty("xmpp.httpbind.scriptSyntax.enabled", false); + } + + /** + * Enables or disables script syntax. + * + * @param isEnabled true to enable script syntax and false to disable it. + * @see #isScriptSyntaxEnabled() + * @see BOSH: Alternative Script + * Syntax + */ + public void setScriptSyntaxEnabled(boolean isEnabled) { + final String property = "xmpp.httpbind.scriptSyntax.enabled"; + if(!isEnabled) { + JiveGlobals.deleteXMLProperty(property); + } + else { + JiveGlobals.setXMLProperty(property, String.valueOf(isEnabled)); + } + } + + private void setUnsecureHttpBindPort(int value) { + if (value == bindPort) { + return; + } + try { + changeHttpBindPorts(value, JiveGlobals.getIntProperty(HTTP_BIND_SECURE_PORT, + HTTP_BIND_SECURE_PORT_DEFAULT)); + bindPort = value; + } + catch (Exception ex) { + Log.error("Error setting HTTP bind ports", ex); + } + } + + private void setSecureHttpBindPort(int value) { + if (value == bindSecurePort) { + return; + } + try { + changeHttpBindPorts(JiveGlobals.getIntProperty(HTTP_BIND_PORT, + HTTP_BIND_PORT_DEFAULT), value); + bindSecurePort = value; + } + catch (Exception ex) { + Log.error("Error setting HTTP bind ports", ex); + } + } + + private synchronized void restartServer() { + if (httpBindServer != null) { + try { + httpBindServer.stop(); + } + catch (Exception e) { + Log.error("Error stopping http bind server", e); + } + + configureHttpBindServer(getHttpBindUnsecurePort(), getHttpBindSecurePort()); + } + } + + /** Listens for changes to Jive properties that affect the HTTP server manager. */ + private class HttpServerPropertyListener implements PropertyEventListener { + + public void propertySet(String property, Map params) { + if (property.equalsIgnoreCase(HTTP_BIND_ENABLED)) { + doEnableHttpBind(Boolean.valueOf(params.get("value").toString())); + } + else if (property.equalsIgnoreCase(HTTP_BIND_PORT)) { + int value; + try { + value = Integer.valueOf(params.get("value").toString()); + } + catch (NumberFormatException ne) { + JiveGlobals.deleteXMLProperty(HTTP_BIND_PORT); + return; + } + setUnsecureHttpBindPort(value); + } + else if (property.equalsIgnoreCase(HTTP_BIND_SECURE_PORT)) { + int value; + try { + value = Integer.valueOf(params.get("value").toString()); + } + catch (NumberFormatException ne) { + JiveGlobals.deleteXMLProperty(HTTP_BIND_SECURE_PORT); + return; + } + setSecureHttpBindPort(value); + } + } + + public void propertyDeleted(String property, Map params) { + if (property.equalsIgnoreCase(HTTP_BIND_ENABLED)) { + doEnableHttpBind(HTTP_BIND_ENABLED_DEFAULT); + } + else if (property.equalsIgnoreCase(HTTP_BIND_PORT)) { + setUnsecureHttpBindPort(HTTP_BIND_PORT_DEFAULT); + } + else if (property.equalsIgnoreCase(HTTP_BIND_SECURE_PORT)) { + setSecureHttpBindPort(HTTP_BIND_SECURE_PORT_DEFAULT); + } + } + + public void xmlPropertySet(String property, Map params) { + } + + public void xmlPropertyDeleted(String property, Map params) { + } + } + + private class JiveSslConnector extends SslSelectChannelConnector { @Override - protected SSLServerSocketFactory createFactory() throws Exception { - return SSLConfig.getServerSocketFactory(); + protected SSLContext createSSLContext() throws Exception { + return SSLConfig.getSSLContext(); + } + } + + private class CertificateListener implements CertificateEventListener { + + public void certificateCreated(KeyStore keyStore, String alias, X509Certificate cert) { + // If new certificate is RSA then (re)start the HTTPS service + if ("RSA".equals(cert.getPublicKey().getAlgorithm())) { + restartServer(); + } + } + + public void certificateDeleted(KeyStore keyStore, String alias) { + restartServer(); + } + + public void certificateSigned(KeyStore keyStore, String alias, + List certificates) { + // If new certificate is RSA then (re)start the HTTPS service + if ("RSA".equals(certificates.get(0).getPublicKey().getAlgorithm())) { + restartServer(); + } } } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java index dea48f5..ff418e8 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java @@ -1,39 +1,50 @@ /** - * $RCSfile$ * $Revision: $ * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), - * a copy of which is included in this distribution. + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserException; -import org.jivesoftware.multiplexer.net.MXParser; import org.jivesoftware.util.Log; +import org.jivesoftware.multiplexer.net.MXParser; import org.dom4j.io.XMPPPacketReader; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.DocumentHelper; import org.mortbay.util.ajax.ContinuationSupport; +import org.apache.commons.lang.StringEscapeUtils; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; +import javax.servlet.ServletConfig; import java.io.IOException; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.net.InetAddress; +import java.net.URLDecoder; /** - * Handles requests to the HTTP Bind service. + * Servlet which handles requests to the HTTP binding service. It determines if there is currently + * an {@link HttpSession} related to the connection or if one needs to be created and then passes it + * off to the {@link HttpBindManager} for processing of the client request and formulating of the + * response. * * @author Alexander Wenckus */ public class HttpBindServlet extends HttpServlet { private HttpSessionManager sessionManager; + private HttpBindManager boshManager; private static XmlPullParserFactory factory; @@ -46,33 +57,86 @@ } } - HttpBindServlet(HttpSessionManager sessionManager) { - this.sessionManager = sessionManager; + private ThreadLocal localReader = new ThreadLocal(); + + public HttpBindServlet() { + } + + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + super.init(servletConfig); + boshManager = HttpBindManager.getInstance(); + sessionManager = boshManager.getSessionManager(); + sessionManager.start(); + } + + + @Override + public void destroy() { + super.destroy(); + sessionManager.stop(); } @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + boolean isScriptSyntaxEnabled = boshManager.isScriptSyntaxEnabled(); + + if(!isScriptSyntaxEnabled) { + sendLegacyError(response, BoshBindingError.itemNotFound); + return; + } + if (isContinuation(request, response)) { return; } + String queryString = request.getQueryString(); + if (queryString == null || "".equals(queryString)) { + sendLegacyError(response, BoshBindingError.badRequest); + return; + } + queryString = URLDecoder.decode(queryString, "utf-8"); + + parseDocument(request, response, new ByteArrayInputStream(queryString.getBytes())); + } + + private void sendLegacyError(HttpServletResponse response, BoshBindingError error) + throws IOException + { + response.sendError(error.getLegacyErrorCode()); + } + + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + if (isContinuation(request, response)) { + return; + } + + parseDocument(request, response, request.getInputStream()); + } + + private void parseDocument(HttpServletRequest request, HttpServletResponse response, + InputStream documentContent) + throws IOException { + Document document; try { - document = createDocument(request); + document = createDocument(documentContent); } catch (Exception e) { Log.warn("Error parsing user request. [" + request.getRemoteAddr() + "]"); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, - "Unable to parse request content: " + e.getMessage()); + sendLegacyError(response, BoshBindingError.badRequest); return; } Element node = document.getRootElement(); if (node == null || !"body".equals(node.getName())) { Log.warn("Body missing from request content. [" + request.getRemoteAddr() + "]"); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, - "Body missing from request content."); + sendLegacyError(response, BoshBindingError.badRequest); return; } @@ -89,16 +153,50 @@ private boolean isContinuation(HttpServletRequest request, HttpServletResponse response) throws IOException { - HttpConnection connection = (HttpConnection) request.getAttribute("request-connection"); - if (connection == null) { + HttpSession session = (HttpSession) request.getAttribute("request-session"); + if (session == null) { return false; } - synchronized (connection.getSession()) { - respond(response, connection); + synchronized (session) { + try { + respond(response, session.getResponse((Long) request.getAttribute("request")), + request.getMethod()); + } + catch (HttpBindException e) { + sendError(request, response, e.getBindingError(), session); + } } return true; } + private void sendError(HttpServletRequest request, HttpServletResponse response, + BoshBindingError bindingError, HttpSession session) + throws IOException + { + try { + if (session.getVersion() >= 1.6) { + respond(response, createErrorBody(bindingError.getErrorType().getType(), + bindingError.getCondition()), request.getMethod()); + } + else { + sendLegacyError(response, bindingError); + } + } + finally { + if (bindingError.getErrorType() == BoshBindingError.Type.terminal) { + session.close(); + } + } + } + + private String createErrorBody(String type, String condition) { + Element body = DocumentHelper.createElement("body"); + body.addNamespace("", "http://jabber.org/protocol/httpbind"); + body.addAttribute("type", type); + body.addAttribute("condition", condition); + return body.asXML(); + } + private void handleSessionRequest(String sid, HttpServletRequest request, HttpServletResponse response, Element rootNode) throws IOException @@ -120,13 +218,10 @@ HttpConnection connection; try { connection = sessionManager.forwardRequest(rid, session, - request.isSecure(), rootNode); + request.isSecure(), rootNode); } catch (HttpBindException e) { - response.sendError(e.getHttpError(), e.getMessage()); - if(e.shouldCloseSession()) { - session.close(); - } + sendError(request, response, e.getBindingError(), session); return; } catch (HttpConnectionClosedException nc) { @@ -137,13 +232,19 @@ String type = rootNode.attributeValue("type"); if ("terminate".equals(type)) { session.close(); - respond(response, createEmptyBody().getBytes("utf-8")); + respond(response, createEmptyBody(), request.getMethod()); } else { - connection - .setContinuation(ContinuationSupport.getContinuation(request, connection)); - request.setAttribute("request-connection", connection); - respond(response, connection); + connection.setContinuation(ContinuationSupport.getContinuation(request, connection)); + request.setAttribute("request-session", connection.getSession()); + request.setAttribute("request", connection.getRequestId()); + try { + respond(response, session.getResponse(connection.getRequestId()), + request.getMethod()); + } + catch (HttpBindException e) { + sendError(request, response, e.getBindingError(), session); + } } } } @@ -160,38 +261,47 @@ try { HttpConnection connection = new HttpConnection(rid, request.isSecure()); - connection.setSession(sessionManager.createSession(rootNode, connection)); - respond(response, connection); + InetAddress address = InetAddress.getByName(request.getRemoteAddr()); + connection.setSession(sessionManager.createSession(address, rootNode, connection)); + respond(response, connection, request.getMethod()); } catch (HttpBindException e) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } + } - private void respond(HttpServletResponse response, HttpConnection connection) + private void respond(HttpServletResponse response, HttpConnection connection, String method) throws IOException { - byte[] content; + String content; try { - content = connection.getDeliverable().getBytes("utf-8"); + content = connection.getResponse(); } catch (HttpBindTimeoutException e) { - content = createEmptyBody().getBytes("utf-8"); + content = createEmptyBody(); } - respond(response, content); + respond(response, content, method); } - private void respond(HttpServletResponse response, byte [] content) throws IOException { + private void respond(HttpServletResponse response, String content, String method) + throws IOException { response.setStatus(HttpServletResponse.SC_OK); - response.setContentType("text/xml"); + response.setContentType("GET".equals(method) ? "text/javascript" : "text/xml"); response.setCharacterEncoding("utf-8"); - response.setContentLength(content.length); - response.getOutputStream().write(content); + if ("GET".equals(method)) { + content = "_BOSH_(\"" + StringEscapeUtils.escapeJavaScript(content) + "\")"; + } + + byte[] byteContent = content.getBytes("utf-8"); + response.setContentLength(byteContent.length); + response.getOutputStream().write(byteContent); + response.getOutputStream().close(); } - private String createEmptyBody() { + private static String createEmptyBody() { Element body = DocumentHelper.createElement("body"); body.addNamespace("", "http://jabber.org/protocol/httpbind"); return body.asXML(); @@ -209,12 +319,20 @@ } } - private Document createDocument(HttpServletRequest request) throws - DocumentException, IOException, XmlPullParserException { + private XMPPPacketReader getPacketReader() { // Reader is associated with a new XMPPPacketReader - XMPPPacketReader reader = new XMPPPacketReader(); - reader.setXPPFactory(factory); + XMPPPacketReader reader = localReader.get(); + if (reader == null) { + reader = new XMPPPacketReader(); + reader.setXPPFactory(factory); + localReader.set(reader); + } + return reader; + } - return reader.read(request.getInputStream()); + private Document createDocument(InputStream request) throws + DocumentException, IOException, XmlPullParserException + { + return getPacketReader().read("utf-8", request); } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindTimeoutException.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindTimeoutException.java index e7e8324..6d6b5fc 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindTimeoutException.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindTimeoutException.java @@ -3,11 +3,13 @@ * $Revision: $ * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), - * a copy of which is included in this distribution. + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; /** diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java index addaab4..ded3895 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java @@ -1,39 +1,47 @@ /** - * $RCSfile: $ - * $Revision: $ - * $Date: $ + * $RCSfile$ + * $Revision: $ + * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. - * This software is the proprietary information of Jive Software. Use is subject to license terms. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; -import org.jivesoftware.multiplexer.Connection; import org.mortbay.util.ajax.Continuation; - /** - * A connection to a client. The client will wait on getDeliverable() until the server forwards a - * message to it or the wait time on the session timesout. + * Represents one HTTP connection with a client using the HTTP Binding service. The client will wait + * on {@link #getResponse()} until the server forwards a message to it or the wait time on the + * session timesout. * * @author Alexander Wenckus */ public class HttpConnection { - private Connection.CompressionPolicy compressionPolicy; private long requestId; private String body; private HttpSession session; private Continuation continuation; private boolean isClosed; private boolean isSecure = false; + private boolean isDelivered; + private static final String CONNECTION_CLOSED = "connection closed"; + + /** + * Constructs an HTTP Connection. + * + * @param requestId the ID which uniquely identifies this request. + * @param isSecure true if this connection is using HTTPS + */ public HttpConnection(long requestId, boolean isSecure) { this.requestId = requestId; this.isSecure = isSecure; - } - - public boolean validate() { - return false; + this.isDelivered = false; } /** @@ -45,21 +53,36 @@ } try { - deliverBody(null); + deliverBody(CONNECTION_CLOSED); } catch (HttpConnectionClosedException e) { /* Shouldn't happen */ } } + /** + * Returns true if this connection has been closed, either a response was delivered to the + * client or the server closed the connection aburbtly. + * + * @return true if this connection has been closed. + */ public boolean isClosed() { return isClosed; } + /** + * Returns true if this connection is using HTTPS. + * + * @return true if this connection is using HTTPS. + */ public boolean isSecure() { return isSecure; } + public boolean isDelivered() { + return isDelivered; + } + /** * Delivers content to the client. The content should be valid XMPP wrapped inside of a body. * A null value for body indicates that the connection should be closed and the client @@ -71,6 +94,9 @@ * a deliverable to forward to the client */ public void deliverBody(String body) throws HttpConnectionClosedException { + if(body == null) { + throw new IllegalArgumentException("Body cannot be null!"); + } // We only want to use this function once so we will close it when the body is delivered. if (isClosed) { throw new HttpConnectionClosedException("The http connection is no longer " + @@ -91,17 +117,16 @@ /** * A call that will cause a wait, or in the case of Jetty the thread to be freed, if there is no - * deliverable currently available. Once the deliverable becomes available it is returned. + * deliverable currently available. Once the response becomes available, it is returned. * * @return the deliverable to send to the client - * * @throws HttpBindTimeoutException to indicate that the maximum wait time requested by the * client has been surpassed and an empty response should be returned. */ - public String getDeliverable() throws HttpBindTimeoutException { + public String getResponse() throws HttpBindTimeoutException { if (body == null && continuation != null) { try { - body = waitForDeliverable(); + body = waitForResponse(); } catch (HttpBindTimeoutException e) { this.isClosed = true; @@ -114,31 +139,11 @@ return body; } - private String waitForDeliverable() throws HttpBindTimeoutException { - if (continuation.suspend(session.getWait() * 1000)) { - String deliverable = (String) continuation.getObject(); - // This will occur when the hold attribute of a session has been exceded. - if (deliverable == null) { - throw new HttpBindTimeoutException(); - } - return deliverable; - } - throw new HttpBindTimeoutException("Request " + requestId + " exceded response time from " + - "server of " + session.getWait() + " seconds."); - } - - public boolean isCompressed() { - return false; - } - - public Connection.CompressionPolicy getCompressionPolicy() { - return compressionPolicy; - } - - public void setCompressionPolicy(Connection.CompressionPolicy compressionPolicy) { - this.compressionPolicy = compressionPolicy; - } - + /** + * Returns the ID which uniquely identifies this connection. + * + * @return the ID which uniquely identifies this connection. + */ public long getRequestId() { return requestId; } @@ -164,4 +169,22 @@ void setContinuation(Continuation continuation) { this.continuation = continuation; } + + private String waitForResponse() throws HttpBindTimeoutException { + if (continuation.suspend(session.getWait() * 1000)) { + String deliverable = (String) continuation.getObject(); + // This will occur when the hold attribute of a session has been exceded. + this.isDelivered = true; + if (deliverable == null) { + throw new HttpBindTimeoutException(); + } + else if(CONNECTION_CLOSED.equals(deliverable)) { + return null; + } + return deliverable; + } + this.isDelivered = true; + throw new HttpBindTimeoutException("Request " + requestId + " exceeded response time from " + + "server of " + session.getWait() + " seconds."); + } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpConnectionClosedException.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpConnectionClosedException.java index d490f89..579fb38 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpConnectionClosedException.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpConnectionClosedException.java @@ -3,11 +3,13 @@ * $Revision: $ * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), - * a copy of which is included in this distribution. + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; /** diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java index ccd26fb..508453c 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java @@ -1,186 +1,149 @@ /** - * $RCSfile$ * $Revision: $ * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), - * a copy of which is included in this distribution. + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; -import org.jivesoftware.multiplexer.*; -import org.jivesoftware.multiplexer.spi.ClientFailoverDeliverer; -import org.dom4j.Element; import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.Namespace; +import org.dom4j.QName; +import org.dom4j.io.XMPPPacketReader; +import org.jivesoftware.multiplexer.ClientSession; +import org.jivesoftware.multiplexer.ConnectionManager; +import org.jivesoftware.multiplexer.net.MXParser; +import org.jivesoftware.util.Log; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import java.io.StringReader; import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; /** * A session represents a serious of interactions with an XMPP client sending packets using the HTTP - * Binding protocol specified in - * XEP-0124. A session can have several - * client connections open simultaneously while awaiting packets bound for the client from the - * server. + * Binding protocol specified in XEP-0124. + * A session can have several client connections open simultaneously while awaiting packets bound + * for the client from the server. * * @author Alexander Wenckus */ -public class HttpSession extends Session { +public class HttpSession extends ClientSession { + private static XmlPullParserFactory factory = null; + private static ThreadLocal localParser = null; + static { + try { + factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null); + factory.setNamespaceAware(true); + } + catch (XmlPullParserException e) { + Log.error("Error creating a parser factory", e); + } + // Create xmpp parser to keep in each thread + localParser = new ThreadLocal() { + protected XMPPPacketReader initialValue() { + XMPPPacketReader parser = new XMPPPacketReader(); + factory.setNamespaceAware(true); + parser.setXPPFactory(factory); + return parser; + } + }; + } + private int wait; - private int hold = -1000; + private int hold = 0; private String language; - private final Queue connectionQueue = new LinkedList(); - private final List pendingElements = new ArrayList(); + private final List connectionQueue = new LinkedList(); + private final List pendingElements = new ArrayList(); + private final List sentElements = new ArrayList(); private boolean isSecure; private int maxPollingInterval; private long lastPoll = -1; - private Set listeners = new HashSet(); - private boolean isClosed; + private Set listeners = new CopyOnWriteArraySet(); + private volatile boolean isClosed; private int inactivityTimeout; + private long lastActivity; + private long lastRequestID; + private int maxRequests; + private Double version = Double.NaN; - protected HttpSession(String serverName, String streamID) { + // Semaphore which protects the packets to send, so, there can only be one consumer at a time. + + private static final Comparator connectionComparator + = new Comparator() { + public int compare(HttpConnection o1, HttpConnection o2) { + return (int) (o1.getRequestId() - o2.getRequestId()); + } + }; + private ConnectionManager connectionManager; + + public HttpSession(String serverName, String streamID, long rid) { super(serverName, null, streamID); + this.lastActivity = System.currentTimeMillis(); + this.lastRequestID = rid; + connectionManager = ConnectionManager.getInstance(); } - void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException, - HttpConnectionClosedException - { - if(connection == null) { - throw new IllegalArgumentException("Connection cannot be null."); + /** + * Returns the stream features which are available for this session. + * + * @return the stream features which are available for this session. + */ + public Collection getAvailableStreamFeaturesElements() { + List elements = new ArrayList(); + + Element sasl = connectionManager.getServerSurrogate().getSASLMechanismsElement(this); + if (sasl != null) { + elements.add(sasl); } - if(isPoll) { - checkPollingInterval(); - } + Element bind = DocumentHelper.createElement(new QName("bind", + new Namespace("", "urn:ietf:params:xml:ns:xmpp-bind"))); + elements.add(bind); - if(isSecure && !connection.isSecure()) { - throw new HttpBindException("Session was started from secure connection, all " + - "connections on this session must be secured.", false, 403); - } - - connection.setSession(this); - if (pendingElements.size() > 0) { - String deliverable = createDeliverable(pendingElements); - pendingElements.clear(); - fireConnectionOpened(connection); - connection.deliverBody(deliverable); - fireConnectionClosed(connection); - } - else { - // With this connection we need to check if we will have too many connections open, - // closing any extras. - while (hold > 0 && connectionQueue.size() >= hold) { - HttpConnection toClose = connectionQueue.remove(); - toClose.close(); - fireConnectionClosed(toClose); - } - connectionQueue.offer(connection); - fireConnectionOpened(connection); - } - } - - private void fireConnectionOpened(HttpConnection connection) { - Collection listeners = - new HashSet(this.listeners); - for(SessionListener listener : listeners) { - listener.connectionOpened(this, connection); - } - } - - private void checkPollingInterval() throws HttpBindException { - long time = System.currentTimeMillis(); - if(lastPoll > 0 && ((time - lastPoll) / 1000) < maxPollingInterval) { - throw new HttpBindException("Too frequent polling minimum interval is " - + maxPollingInterval + ", current interval " + ((lastPoll - time) / 1000), - true, 403); - } - lastPoll = time; + Element session = DocumentHelper.createElement(new QName("session", + new Namespace("", "urn:ietf:params:xml:ns:xmpp-session"))); + elements.add(session); + return elements; } public String getAvailableStreamFeatures() { - return null; + StringBuilder sb = new StringBuilder(200); + for (Element element : getAvailableStreamFeaturesElements()) { + sb.append(element.asXML()); + } + return sb.toString(); } - public synchronized void close() { - close(false); + public void close() { + closeConnection(); } - public synchronized void close(boolean isServerShuttingDown) { - if(isClosed) { - return; - } - isClosed = true; - - if(pendingElements.size() > 0) { - failDelivery(); - } - - Collection listeners = - new HashSet(this.listeners); - this.listeners.clear(); - for(SessionListener listener : listeners) { - listener.sessionClosed(this); - } + public void close(boolean isServerShuttingDown) { + closeConnection(); } - private void failDelivery() { - ClientFailoverDeliverer deliverer = new ClientFailoverDeliverer(); - deliverer.setStreamID(getStreamID()); - for(Element element : pendingElements) { - deliverer.deliver(element); - } - pendingElements.clear(); - } - + /** + * Returns true if this session has been closed and no longer activley accepting connections. + * + * @return true if this session has been closed and no longer activley accepting connections. + */ public synchronized boolean isClosed() { return isClosed; } - public synchronized void deliver(Element stanza) { - String deliverable = createDeliverable(Arrays.asList(stanza)); - boolean delivered = false; - while(!delivered && connectionQueue.size() > 0) { - HttpConnection connection = connectionQueue.remove(); - try { - connection.deliverBody(deliverable); - delivered = true; - fireConnectionClosed(connection); - } - catch (HttpConnectionClosedException e) { - /* Connection was closed, try the next one */ - } - } - - if(!delivered) { - pendingElements.add(stanza); - } - } - - private void fireConnectionClosed(HttpConnection connection) { - Collection listeners = - new HashSet(this.listeners); - for(SessionListener listener : listeners) { - listener.connectionClosed(this, connection); - } - } - - private String createDeliverable(Collection elements) { - Element body = DocumentHelper.createElement("body"); - body.addNamespace("", "http://jabber.org/protocol/httpbind"); - for(Element child : elements) { - child = child.createCopy(); - child.setParent(null); - body.add(child); - } - return body.asXML(); - } - /** - * This attribute specifies the longest time (in seconds) that the connection manager is allowed - * to wait before responding to any request during the session. This enables the client to - * prevent its TCP connection from expiring due to inactivity, as well as to limit the delay - * before it discovers any network failure. + * Specifies the longest time (in seconds) that the connection manager is allowed to wait before + * responding to any request during the session. This enables the client to prevent its TCP + * connection from expiring due to inactivity, as well as to limit the delay before it discovers + * any network failure. * * @param wait the longest time it is permissible to wait for a response. */ @@ -189,10 +152,10 @@ } /** - * This attribute specifies the longest time (in seconds) that the connection manager is allowed - * to wait before responding to any request during the session. This enables the client to - * prevent its TCP connection from expiring due to inactivity, as well as to limit the delay - * before it discovers any network failure. + * Specifies the longest time (in seconds) that the connection manager is allowed to wait before + * responding to any request during the session. This enables the client to prevent its TCP + * connection from expiring due to inactivity, as well as to limit the delay before it discovers + * any network failure. * * @return the longest time it is permissible to wait for a response. */ @@ -201,23 +164,22 @@ } /** - * This attribute specifies the maximum number of requests the connection manager is allowed - * to keep waiting at any one time during the session. (For example, if a constrained client - * is unable to keep open more than two HTTP connections to the same HTTP server simultaneously, - * then it SHOULD specify a value of "1".) + * Specifies the maximum number of requests the connection manager is allowed to keep waiting at + * any one time during the session. (For example, if a constrained client is unable to keep open + * more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify + * a value of "1".) * * @param hold the maximum number of simultaneous waiting requests. - * */ public void setHold(int hold) { this.hold = hold; } /** - * This attribute specifies the maximum number of requests the connection manager is allowed - * to keep waiting at any one time during the session. (For example, if a constrained client - * is unable to keep open more than two HTTP connections to the same HTTP server simultaneously, - * then it SHOULD specify a value of "1".) + * Specifies the maximum number of requests the connection manager is allowed to keep waiting at + * any one time during the session. (For example, if a constrained client is unable to keep open + * more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify + * a value of "1".) * * @return the maximum number of simultaneous waiting requests */ @@ -225,10 +187,20 @@ return hold; } + /** + * Sets the language this session is using. + * + * @param language the language this session is using. + */ public void setLanaguage(String language) { this.language = language; } + /** + * Returns the language this session is using. + * + * @return the language this session is using. + */ public String getLanguage() { return language; } @@ -245,6 +217,186 @@ } /** + * Returns the max interval within which a client can send polling requests. If more than one + * request occurs in the interval the session will be terminated. + * + * @return the max interval within which a client can send polling requests. If more than one + * request occurs in the interval the session will be terminated. + */ + public int getMaxPollingInterval() { + return this.maxPollingInterval; + } + + /** + * The max number of requests it is permissable for this session to have open at any one time. + * + * @param maxRequests The max number of requests it is permissable for this session to have open + * at any one time. + */ + public void setMaxRequests(int maxRequests) { + this.maxRequests = maxRequests; + } + + /** + * Returns the max number of requests it is permissable for this session to have open at any one + * time. + * + * @return the max number of requests it is permissable for this session to have open at any one + * time. + */ + public int getMaxRequests() { + return this.maxRequests; + } + + /** + * Returns true if all connections on this session should be secured, and false if they should + * not. + * + * @return true if all connections on this session should be secured, and false if they should + * not. + */ + public boolean isSecure() { + return isSecure; + } + + /** + * Adds a {@link SessionListener} to this session. The listener + * will be notified of changes to the session. + * + * @param listener the listener which is being added to the session. + */ + public void addSessionCloseListener(SessionListener listener) { + listeners.add(listener); + } + + /** + * Removes a {@link SessionListener} from this session. The + * listener will no longer be updated when an event occurs on the session. + * + * @param listener the session listener that is to be removed. + */ + public void removeSessionCloseListener(SessionListener listener) { + listeners.remove(listener); + } + + /** + * Sets the time, in seconds, after which this session will be considered inactive and be be + * terminated. + * + * @param inactivityTimeout the time, in seconds, after which this session will be considered + * inactive and be terminated. + */ + public void setInactivityTimeout(int inactivityTimeout) { + this.inactivityTimeout = inactivityTimeout; + } + + /** + * Returns the time, in seconds, after which this session will be considered inactive and + * terminated. + * + * @return the time, in seconds, after which this session will be considered inactive and + * terminated. + */ + public int getInactivityTimeout() { + return inactivityTimeout; + } + + /** + * Returns the time in milliseconds since the epoch that this session was last active. Activity + * is a request was either made or responded to. If the session is currently active, meaning + * there are connections awaiting a response, the current time is returned. + * + * @return the time in milliseconds since the epoch that this session was last active. + */ + public synchronized long getLastActivity() { + if (connectionQueue.isEmpty()) { + return lastActivity; + } + else { + for (HttpConnection connection : connectionQueue) { + // The session is currently active, return the current time. + if (!connection.isClosed()) { + return System.currentTimeMillis(); + } + } + // We have no currently open connections therefore we can assume that lastActivity is + // the last time the client did anything. + return lastActivity; + } + } + + /** + * Sets the version of BOSH which the client implements. Currently, the only versions supported + * by Openfire are 1.5 and 1.6. Any versions less than or equal to 1.5 will be interpreted as + * 1.5 and any values greater than or equal to 1.6 will be interpreted as 1.6. + * + * @param version the version of BOSH which the client implements, represented as a Double, + * {major version}.{minor version}. + */ + public void setVersion(double version) { + if(version <= 1.5) { + return; + } + else if(version >= 1.6) { + version = 1.6; + } + this.version = version; + } + + /** + * Returns the BOSH version which this session utilizes. The version refers to the + * version of the XEP which the connecting client implements. If the client did not specify + * a version 1.5 is returned as this is the last version of the XEP that the client was not + * required to pass along its version information when creating a session. + * + * @return the version of the BOSH XEP which the client is utilizing. + */ + public double getVersion() { + if (!Double.isNaN(this.version)) { + return this.version; + } + else { + return 1.5; + } + } + + public String getResponse(long requestID) throws HttpBindException { + for (HttpConnection connection : connectionQueue) { + if (connection.getRequestId() == requestID) { + String response = getResponse(connection); + + // connection needs to be removed after response is returned to maintain idempotence + // otherwise if this method is called again, after 'waiting', the InternalError + // will be thrown because the connection is no longer in the queue. + connectionQueue.remove(connection); + fireConnectionClosed(connection); + return response; + } + } + throw new InternalError("Could not locate connection: " + requestID); + } + + private String getResponse(HttpConnection connection) throws HttpBindException { + String response = null; + try { + response = connection.getResponse(); + } + catch (HttpBindTimeoutException e) { + // This connection timed out we need to increment the request count + if (connection.getRequestId() != lastRequestID + 1) { + throw new HttpBindException("Unexpected RID error.", + BoshBindingError.itemNotFound); + } + lastRequestID = connection.getRequestId(); + } + if (response == null) { + response = createEmptyBody(); + } + return response; + } + + /** * Sets whether the initial request on the session was secure. * * @param isSecure true if the initial request was secure and false if it wasn't. @@ -254,33 +406,345 @@ } /** - * Returns true if all connections on this session should be secured, and false if - * they should not. + * Creates a new connection on this session. If a response is currently available for this + * session the connection is responded to immediately, otherwise it is queued awaiting a + * response. * - * @return true if all connections on this session should be secured, and false if - * they should not. + * @param rid the request id related to the connection. + * @param packetsToBeSent any packets that this connection should send. + * @param isSecure true if the connection was secured using HTTPS. + * @return the created {@link HttpConnection} which represents + * the connection. + * + * @throws HttpConnectionClosedException if the connection was closed before a response could be + * delivered. + * @throws HttpBindException if the connection has violated a facet of the HTTP binding + * protocol. */ - public boolean isSecure() { - return isSecure; + synchronized HttpConnection createConnection(long rid, Collection packetsToBeSent, + boolean isSecure) + throws HttpConnectionClosedException, HttpBindException + { + HttpConnection connection = new HttpConnection(rid, isSecure); + if (rid <= lastRequestID) { + Delivered deliverable = retrieveDeliverable(rid); + if (deliverable == null) { + Log.warn("Deliverable unavailable for " + rid); + throw new HttpBindException("Unexpected RID error.", + BoshBindingError.itemNotFound); + } + connection.deliverBody(createDeliverable(deliverable.deliverables)); + return connection; + } + else if (rid > (lastRequestID + hold + 1)) { + // TODO handle the case of greater RID which basically has it wait + Log.warn("Request " + rid + " > " + (lastRequestID + hold + 1) + ", ending session."); + throw new HttpBindException("Unexpected RID error.", + BoshBindingError.itemNotFound); + } + + addConnection(connection, packetsToBeSent.size() <= 0); + return connection; } - public void addSessionCloseListener(SessionListener listener) { - listeners.add(listener); + private Delivered retrieveDeliverable(long rid) { + for (Delivered delivered : sentElements) { + if (delivered.getRequestID() == rid) { + return delivered; + } + } + return null; } - public void removeSessionCloseListener(SessionListener listener) { - listeners.remove(listener); + private void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException, + HttpConnectionClosedException { + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null."); + } + + if (isPoll) { + checkPollingInterval(); + } + + if (isSecure && !connection.isSecure()) { + throw new HttpBindException("Session was started from secure connection, all " + + "connections on this session must be secured.", BoshBindingError.badRequest); + } + + connection.setSession(this); + // We aren't supposed to hold connections open or we already have some packets waiting + // to be sent to the client. + if (hold <= 0 || (pendingElements.size() > 0 && connection.getRequestId() == lastRequestID + 1)) { + deliver(connection, pendingElements); + lastRequestID = connection.getRequestId(); + pendingElements.clear(); + } + else { + // With this connection we need to check if we will have too many connections open, + // closing any extras. + + // Number of current connections open plus the current one tells us how many that + // we need to close. + int connectionsToClose = (getOpenConnectionCount() + 1) - hold; + int closed = 0; + for (int i = 0; i < connectionQueue.size() && closed < connectionsToClose; i++) { + HttpConnection toClose = connectionQueue.get(i); + if (!toClose.isClosed()) { + lastRequestID = toClose.getRequestId(); + toClose.close(); + closed++; + } + } + } + connectionQueue.add(connection); + Collections.sort(connectionQueue, connectionComparator); + fireConnectionOpened(connection); } - public void setInactivityTimeout(int inactivityTimeout) { - this.inactivityTimeout = inactivityTimeout; + private int getOpenConnectionCount() { + int count = 0; + for (HttpConnection connection : connectionQueue) { + if (!connection.isClosed()) { + count++; + } + } + return count; } - public int getInactivityTimeout() { - return inactivityTimeout; + private void deliver(HttpConnection connection, Collection deliverable) + throws HttpConnectionClosedException { + connection.deliverBody(createDeliverable(deliverable)); + + Delivered delivered = new Delivered(deliverable); + delivered.setRequestID(connection.getRequestId()); + while (sentElements.size() > hold) { + sentElements.remove(0); + } + + sentElements.add(delivered); } - public int getConnectionCount() { - return connectionQueue.size(); + private void fireConnectionOpened(HttpConnection connection) { + lastActivity = System.currentTimeMillis(); + for (SessionListener listener : listeners) { + listener.connectionOpened(this, connection); + } + } + + private void checkPollingInterval() throws HttpBindException { + long time = System.currentTimeMillis(); + if (((time - lastPoll) / 1000) < maxPollingInterval) { + throw new HttpBindException("Too frequent polling minimum interval is " + + maxPollingInterval + ", current interval " + ((time - lastPoll) / 1000), + BoshBindingError.policyViolation); + } + lastPoll = time; + } + + public void deliver(Element stanza) { + deliver(new Deliverable(Arrays.asList(stanza))); + } + + private synchronized void deliver(Deliverable stanza) { + Collection deliverable = Arrays.asList(stanza); + boolean delivered = false; + for (HttpConnection connection : connectionQueue) { + try { + if (connection.getRequestId() == lastRequestID + 1) { + lastRequestID = connection.getRequestId(); + deliver(connection, deliverable); + delivered = true; + break; + } + } + catch (HttpConnectionClosedException e) { + /* Connection was closed, try the next one */ + } + } + + if (!delivered) { + pendingElements.add(stanza); + } + } + + private void fireConnectionClosed(HttpConnection connection) { + lastActivity = System.currentTimeMillis(); + for (SessionListener listener : listeners) { + listener.connectionClosed(this, connection); + } + } + + private String createDeliverable(Collection elements) { + StringBuilder builder = new StringBuilder(); + builder.append(""); + for (Deliverable child : elements) { + builder.append(child.getDeliverable()); + } + builder.append(""); + return builder.toString(); + } + + private synchronized void closeConnection() { + if (isClosed) { + return; + } + isClosed = true; + + if (pendingElements.size() > 0) { + failDelivery(); + } + + for (SessionListener listener : listeners) { + listener.sessionClosed(this); + } + this.listeners.clear(); + } + + private void failDelivery() { + for (Deliverable deliverable : pendingElements) { + Collection packet = deliverable.getPackets(); + if (packet != null) { + failDelivery(packet); + } + } + + for (HttpConnection toClose : connectionQueue) { + if (!toClose.isDelivered()) { + Delivered delivered = retrieveDeliverable(toClose.getRequestId()); + if (delivered != null) { + failDelivery(delivered.getPackets()); + } + else { + Log.warn("Packets could not be found for session " + getStreamID() + " cannot" + + "be delivered to client"); + } + } + toClose.close(); + fireConnectionClosed(toClose); + } + pendingElements.clear(); + } + + private void failDelivery(Collection packets) { + if (packets == null) { + // Do nothing if someone asked to deliver nothing :) + return; + } + for (Element packet : packets) { + // Inform the server that the wrapped stanza was not delivered + String tag = packet.getName(); + if ("message".equals(tag)) { + connectionManager.getServerSurrogate().deliveryFailed(packet, getStreamID()); + } + else if ("iq".equals(tag)) { + String type = packet.attributeValue("type", "get"); + if ("get".equals(type) || "set".equals(type)) { + // Build IQ of type ERROR + Element reply = packet.createCopy(); + reply.addAttribute("type", "error"); + reply.addAttribute("from", packet.attributeValue("to")); + reply.addAttribute("to", packet.attributeValue("from")); + Element error = reply.addElement("error"); + error.addAttribute("type", "wait"); + error.addElement("unexpected-request") + .addAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-stanzas"); + // Bounce the failed IQ packet + connectionManager.getServerSurrogate().send(reply.asXML(), getStreamID()); + } + } + } + } + + + private static String createEmptyBody() { + Element body = DocumentHelper.createElement("body"); + body.addNamespace("", "http://jabber.org/protocol/httpbind"); + return body.asXML(); + } + + private class Deliverable implements Comparable { + private final String text; + private final Collection packets; + private long requestID; + + public Deliverable(String text) { + this.text = text; + this.packets = null; + } + + public Deliverable(Collection elements) { + this.text = null; + this.packets = new ArrayList(); + for (Element packet : elements) { + this.packets.add(packet.asXML()); + } + } + + public String getDeliverable() { + if (text == null) { + StringBuilder builder = new StringBuilder(); + for (String packet : packets) { + builder.append(packet); + } + return builder.toString(); + } + else { + return text; + } + } + + public void setRequestID(long requestID) { + this.requestID = requestID; + } + + public long getRequestID() { + return requestID; + } + + public Collection getPackets() { + List answer = new ArrayList(); + for (String packetXML : packets) { + try { + // Parse the XML stanza + Element element = localParser.get().read(new StringReader(packetXML)).getRootElement(); + answer.add(element); + } + catch (Exception e) { + Log.error("Error while parsing Privacy Property", e); + } + } + return answer; + } + + public int compareTo(Deliverable o) { + return (int) (o.getRequestID() - requestID); + } + } + + private class Delivered { + private long requestID; + private Collection deliverables; + + public Delivered(Collection deliverables) { + this.deliverables = deliverables; + } + + public void setRequestID(long requestID) { + this.requestID = requestID; + } + + public long getRequestID() { + return requestID; + } + + public Collection getPackets() { + List packets = new ArrayList(); + for (Deliverable deliverable : deliverables) { + if (deliverable.packets != null) { + packets.addAll(deliverable.getPackets()); + } + } + return packets; + } } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java index eeffc32..a2f8a61 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java @@ -1,107 +1,135 @@ /** - * $RCSfile$ * $Revision: $ * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), - * a copy of which is included in this distribution. + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; -import org.dom4j.*; -import org.jivesoftware.multiplexer.ConnectionManager; -import org.jivesoftware.multiplexer.ServerSurrogate; -import org.jivesoftware.multiplexer.Session; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.jivesoftware.util.JiveConstants; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; +import org.jivesoftware.util.TaskEngine; +import org.jivesoftware.multiplexer.StreamIDFactory; +import org.jivesoftware.multiplexer.ServerSurrogate; +import org.jivesoftware.multiplexer.ConnectionManager; +import org.jivesoftware.multiplexer.Session; -import java.util.*; +import java.net.InetAddress; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; +import java.util.concurrent.*; /** - * + * Manages sessions for all users connecting to Openfire using the HTTP binding protocal, + * XEP-0124. */ public class HttpSessionManager { + public static StreamIDFactory idFactory = new StreamIDFactory(); + protected static String serverName = ConnectionManager.getInstance().getServerName(); - /** - * Milliseconds a connection has to be idle to be closed. Default is 30 minutes. Sending - * stanzas to the client is not considered as activity. We are only considering the connection - * active when the client sends some data or hearbeats (i.e. whitespaces) to the server. - * The reason for this is that sending data will fail if the connection is closed. And if - * the thread is blocked while sending data (because the socket is closed) then the clean up - * thread will close the socket anyway. - */ - private static int inactivityTimeout; - - /** - * The connection manager MAY limit the number of simultaneous requests the client makes with - * the 'requests' attribute. The RECOMMENDED value is "2". Servers that only support polling - * behavior MUST prevent clients from making simultaneous requests by setting the 'requests' - * attribute to a value of "1" (however, polling is NOT RECOMMENDED). In any case, clients MUST - * NOT make more simultaneous requests than specified by the connection manager. - */ - private static int maxRequests; - - /** - * The connection manager SHOULD include two additional attributes in the session creation - * response element, specifying the shortest allowable polling interval and the longest - * allowable inactivity period (both in seconds). Communication of these parameters enables - * the client to engage in appropriate behavior (e.g., not sending empty request elements more - * often than desired, and ensuring that the periods with no requests pending are - * never too long). - */ - private static int pollingInterval; - - private String serverName; private ServerSurrogate serverSurrogate; - private InactivityTimer timer = new InactivityTimer(); + private Map sessionMap = new ConcurrentHashMap(); + private TimerTask inactivityTask; + private SessionListener sessionListener = new SessionListener() { + public void connectionOpened(HttpSession session, HttpConnection connection) { + } - static { - // Set the default read idle timeout. If none was set then assume 30 minutes - inactivityTimeout = JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30); - maxRequests = JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.max", 2); - pollingInterval = JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.polling", 5); - } + public void connectionClosed(HttpSession session, HttpConnection connection) { + } - public HttpSessionManager(String serverName) { - this.serverName = serverName; + public void sessionClosed(HttpSession session) { + Session.removeSession(session.getStreamID()); + sessionMap.remove(session.getStreamID()); + serverSurrogate.clientSessionClosed(session.getStreamID()); + } + }; + + /** + * Creates a new HttpSessionManager instance. + */ + public HttpSessionManager() { this.serverSurrogate = ConnectionManager.getInstance().getServerSurrogate(); } - public HttpSession getSession(String streamID) { - Session session = Session.getSession(streamID); - if(session instanceof HttpSession) { - return (HttpSession) session; - } - return null; + /** + * Starts the services used by the HttpSessionManager. + */ + public void start() { + inactivityTask = new HttpSessionReaper(); + TaskEngine.getInstance().schedule(inactivityTask, 30 * JiveConstants.SECOND, + 30 * JiveConstants.SECOND); } - public HttpSession createSession(Element rootNode, HttpConnection connection) - throws HttpBindException - { + /** + * Stops any services and cleans up any resources used by the HttpSessionManager. + */ + public void stop() { + inactivityTask.cancel(); + for (HttpSession session : sessionMap.values()) { + session.close(); + } + sessionMap.clear(); + } + + /** + * Returns the session related to a stream id. + * + * @param streamID the stream id to retrieve the session. + * @return the session related to the provided stream id. + */ + public HttpSession getSession(String streamID) { + return sessionMap.get(streamID); + } + + /** + * Creates an HTTP binding session which will allow a user to exchange packets with Openfire. + * + * @param address the internet address that was used to bind to Wildfie. + * @param rootNode the body element that was sent containing the request for a new session. + * @param connection the HTTP connection object which abstracts the individual connections to + * Openfire over the HTTP binding protocol. The initial session creation response is returned to + * this connection. + * @return the created HTTP session. + * + * Either shutting down or starting up. + * @throws HttpBindException when there is an internal server error related to the creation of + * the initial session creation response. + */ + public HttpSession createSession(InetAddress address, Element rootNode, + HttpConnection connection) + throws HttpBindException { // TODO Check if IP address is allowed to connect to the server // Default language is English ("en"). String language = rootNode.attributeValue("xml:lang"); - if(language == null || "".equals(language)) { + if (language == null || "".equals(language)) { language = "en"; } int wait = getIntAttribute(rootNode.attributeValue("wait"), 60); int hold = getIntAttribute(rootNode.attributeValue("hold"), 1); + double version = getDoubleAttribute(rootNode.attributeValue("ver"), 1.5); - // Indicate the compression policy to use for this connection - connection.setCompressionPolicy(serverSurrogate.getCompressionPolicy()); - - HttpSession session = createSession(serverName); - session.setWait(wait); + HttpSession session = createSession(connection.getRequestId(), address); + session.setWait(Math.min(wait, getMaxWait())); session.setHold(hold); session.setSecure(connection.isSecure()); - session.setMaxPollingInterval(pollingInterval); - session.setInactivityTimeout(inactivityTimeout); + session.setMaxPollingInterval(getPollingInterval()); + session.setMaxRequests(getMaxRequests()); + session.setInactivityTimeout(getInactivityTimeout()); // Store language and version information in the connection. session.setLanaguage(language); + session.setVersion(version); try { connection.deliverBody(createSessionCreationResponse(session)); } @@ -110,50 +138,114 @@ } catch (DocumentException e) { Log.error("Error creating document", e); - throw new HttpBindException("Internal server error", true, 500); + throw new HttpBindException("Internal server error", + BoshBindingError.internalServerError); } - - timer.reset(session); return session; } - private HttpSession createSession(String serverName) { + + /** + * Returns the longest time (in seconds) that Openfire is allowed to wait before responding to + * any request during the session. This enables the client to prevent its TCP connection from + * expiring due to inactivity, as well as to limit the delay before it discovers any network + * failure. + * + * @return the longest time (in seconds) that Openfire is allowed to wait before responding to + * any request during the session. + */ + public int getMaxWait() { + return JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.wait", + Integer.MAX_VALUE); + } + + /** + * Openfire SHOULD include two additional attributes in the session creation response element, + * specifying the shortest allowable polling interval and the longest allowable inactivity + * period (both in seconds). Communication of these parameters enables the client to engage in + * appropriate behavior (e.g., not sending empty request elements more often than desired, and + * ensuring that the periods with no requests pending are never too long). + * + * @return the maximum allowable period over which a client can send empty requests to the + * server. + */ + public int getPollingInterval() { + return JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.polling", 5); + } + + /** + * Openfire MAY limit the number of simultaneous requests the client makes with the 'requests' + * attribute. The RECOMMENDED value is "2". Servers that only support polling behavior MUST + * prevent clients from making simultaneous requests by setting the 'requests' attribute to a + * value of "1" (however, polling is NOT RECOMMENDED). In any case, clients MUST NOT make more + * simultaneous requests than specified by the Openfire. + * + * @return the number of simultaneous requests allowable. + */ + public int getMaxRequests() { + return JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.max", 2); + } + + /** + * Seconds a session has to be idle to be closed. Default is 30 minutes. Sending stanzas to the + * client is not considered as activity. We are only considering the connection active when the + * client sends some data or hearbeats (i.e. whitespaces) to the server. The reason for this is + * that sending data will fail if the connection is closed. And if the thread is blocked while + * sending data (because the socket is closed) then the clean up thread will close the socket + * anyway. + * + * @return Seconds a session has to be idle to be closed. + */ + public int getInactivityTimeout() { + return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30); + } + + /** + * Forwards a client request, which is related to a session, to the server. A connection is + * created and queued up in the provided session. When a connection reaches the top of a queue + * any pending packets bound for the client will be forwarded to the client through the + * connection. + * + * @param rid the unique, sequential, requestID sent from the client. + * @param session the HTTP session of the client that made the request. + * @param isSecure true if the request was made over a secure channel, HTTPS, and false if it + * was not. + * @param rootNode the XML body of the request. + * @return the created HTTP connection. + * + * @throws HttpBindException for several reasons: if the encoding inside of an auth packet is + * not recognized by the server, or if the packet type is not recognized. + * @throws HttpConnectionClosedException if the session is no longer available. + */ + public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure, + Element rootNode) throws HttpBindException, + HttpConnectionClosedException + { + //noinspection unchecked + List elements = rootNode.elements(); + HttpConnection connection = session.createConnection(rid, elements, isSecure); + for (Element packet : elements) { + serverSurrogate.send(packet.asXML(), session.getStreamID()); + } + return connection; + } + + private HttpSession createSession(long rid, InetAddress address) { // Create a ClientSession for this user. - String streamID = Session.idFactory.createStreamID(); - HttpSession session = new HttpSession(serverName, streamID); - // Register that the new session is associated with the specified stream ID - HttpSession.addSession(streamID, session); + String streamID = idFactory.createStreamID(); // Send to the server that a new client session has been created - serverSurrogate.clientSessionCreated(streamID); - session.addSessionCloseListener(new SessionListener() { - public void connectionOpened(Session session, HttpConnection connection) { - if (session instanceof HttpSession) { - timer.stop((HttpSession) session); - } - } - - public void connectionClosed(Session session, HttpConnection connection) { - if(session instanceof HttpSession) { - HttpSession http = (HttpSession) session; - if(http.getConnectionCount() <= 0) { - timer.reset(http); - } - } - } - - public void sessionClosed(Session session) { - HttpSession.removeSession(session.getStreamID()); - if(session instanceof HttpSession) { - timer.stop((HttpSession) session); - } - serverSurrogate.clientSessionClosed(session.getStreamID()); - } - }); + HttpSession session = new HttpSession(serverName, streamID, rid); + // Register that the new session is associated with the specified stream ID + sessionMap.put(streamID, session); + Session.addSession(streamID, session); + session.addSessionCloseListener(sessionListener); + // Send to the server that a new client session has been created + serverSurrogate.clientSessionCreated(streamID, address); return session; } private static int getIntAttribute(String value, int defaultValue) { - if(value == null || "".equals(value)) { + if (value == null || "".equals(value.trim())) { return defaultValue; } try { @@ -164,6 +256,18 @@ } } + private double getDoubleAttribute(String doubleValue, double defaultValue) { + if (doubleValue == null || "".equals(doubleValue.trim())) { + return defaultValue; + } + try { + return Double.parseDouble(doubleValue); + } + catch (Exception ex) { + return defaultValue; + } + } + private String createSessionCreationResponse(HttpSession session) throws DocumentException { Element response = DocumentHelper.createElement("body"); response.addNamespace("", "http://jabber.org/protocol/httpbind"); @@ -171,76 +275,32 @@ response.addAttribute("authid", session.getStreamID()); response.addAttribute("sid", session.getStreamID()); response.addAttribute("secure", Boolean.TRUE.toString()); - response.addAttribute("requests", String.valueOf(maxRequests)); + response.addAttribute("requests", String.valueOf(session.getMaxRequests())); response.addAttribute("inactivity", String.valueOf(session.getInactivityTimeout())); - response.addAttribute("polling", String.valueOf(pollingInterval)); + response.addAttribute("polling", String.valueOf(session.getMaxPollingInterval())); response.addAttribute("wait", String.valueOf(session.getWait())); + if(session.getVersion() >= 1.6) { + response.addAttribute("ver", String.valueOf(session.getVersion())); + } Element features = response.addElement("stream:features"); - - features.add(serverSurrogate.getSASLMechanismsElement(session).createCopy()); - - Element bind = DocumentHelper.createElement(new QName("bind", - new Namespace("", "urn:ietf:params:xml:ns:xmpp-bind"))); - features.add(bind); - - Element sessionElement = DocumentHelper.createElement(new QName("session", - new Namespace("", "urn:ietf:params:xml:ns:xmpp-session"))); - features.add(sessionElement); + for (Element feature : session.getAvailableStreamFeaturesElements()) { + features.add(feature.createCopy()); + } return response.asXML(); } - public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure, - Element rootNode) throws HttpBindException, - HttpConnectionClosedException - { - - //noinspection unchecked - List elements = rootNode.elements(); - boolean isPoll = elements.size() <= 0; - HttpConnection connection = new HttpConnection(rid, isSecure); - session.addConnection(connection, isPoll); - - for (Element packet : elements) { - serverSurrogate.send(packet.asXML(), session.getStreamID()); - } - - return connection; - } - - private class InactivityTimer extends Timer { - private Map sessionMap - = new HashMap(); - - public void stop(HttpSession session) { - InactivityTimeoutTask task = sessionMap.remove(session.getStreamID()); - if(task != null) { - task.cancel(); - } - } - - public void reset(HttpSession session) { - stop(session); - if(session.isClosed()) { - return; - } - InactivityTimeoutTask task = new InactivityTimeoutTask(session); - schedule(task, session.getInactivityTimeout() * 1000); - sessionMap.put(session.getStreamID(), task); - } - } - - private class InactivityTimeoutTask extends TimerTask { - private Session session; - - public InactivityTimeoutTask(Session session) { - this.session = session; - } + private class HttpSessionReaper extends TimerTask { public void run() { - session.close(); - timer.sessionMap.remove(session.getStreamID()); + long currentTime = System.currentTimeMillis(); + for (HttpSession session : sessionMap.values()) { + long lastActive = (currentTime - session.getLastActivity()) / 1000; + if (lastActive > session.getInactivityTimeout()) { + session.close(); + } + } } } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/SessionListener.java b/src/java/org/jivesoftware/multiplexer/net/http/SessionListener.java index 699841a..46e5f17 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/SessionListener.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/SessionListener.java @@ -1,22 +1,44 @@ /** - * $RCSfile: $ - * $Revision: $ - * $Date: $ + * $RCSfile$ + * $Revision: $ + * $Date: $ * - * Copyright (C) 2006 Jive Software. All rights reserved. - * This software is the proprietary information of Jive Software. Use is subject to license terms. + * Copyright (C) 2005-2008 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. */ + package org.jivesoftware.multiplexer.net.http; -import org.jivesoftware.multiplexer.Session; - /** + * Listens for HTTP binding session events. * + * @author Alexander Wenckus */ public interface SessionListener { - public void connectionOpened(Session session, HttpConnection connection); - public void connectionClosed(Session session, HttpConnection connection); + /** + * A connection was opened. + * + * @param session the session. + * @param connection the connection. + */ + public void connectionOpened(HttpSession session, HttpConnection connection); - public void sessionClosed(Session session); -} + /** + * A conneciton was closed. + * + * @param session the session. + * @param connection the connection. + */ + public void connectionClosed(HttpSession session, HttpConnection connection); + + /** + * A session ended. + * + * @param session the session. + */ + public void sessionClosed(HttpSession session); +} \ No newline at end of file diff --git a/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java b/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java index ea87e20..a77d87a 100644 --- a/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java +++ b/src/java/org/jivesoftware/multiplexer/task/NewSessionTask.java @@ -14,6 +14,8 @@ import org.jivesoftware.multiplexer.ConnectionWorkerThread; import org.jivesoftware.multiplexer.ClientSession; +import java.net.InetAddress; + /** * Task that notifies the server that a new client session has been created. This task * is executed right after clients send their initial stream header. @@ -22,13 +24,16 @@ */ public class NewSessionTask extends ClientTask { - public NewSessionTask(String streamID) { + private InetAddress address; + + public NewSessionTask(String streamID, InetAddress address) { super(streamID); + this.address = address; } public void run() { ConnectionWorkerThread workerThread = (ConnectionWorkerThread) Thread.currentThread(); - workerThread.clientSessionCreated(streamID); + workerThread.clientSessionCreated(streamID, address); } public void serverNotAvailable() { diff --git a/src/java/org/jivesoftware/util/PropertyEventListener.java b/src/java/org/jivesoftware/util/PropertyEventListener.java index 9121d12..6cdb85a 100644 --- a/src/java/org/jivesoftware/util/PropertyEventListener.java +++ b/src/java/org/jivesoftware/util/PropertyEventListener.java @@ -28,7 +28,7 @@ * @param property the property. * @param params event parameters. */ - public void propertySet(String property, Map params); + public void propertySet(String property, Map params); /** * A property was deleted. @@ -36,7 +36,7 @@ * @param property the deleted. * @param params event parameters. */ - public void propertyDeleted(String property, Map params); + public void propertyDeleted(String property, Map params); /** * An XML property was set. @@ -44,7 +44,7 @@ * @param property the property. * @param params event parameters. */ - public void xmlPropertySet(String property, Map params); + public void xmlPropertySet(String property, Map params); /** * An XML property was deleted. @@ -52,6 +52,6 @@ * @param property the property. * @param params event parameters. */ - public void xmlPropertyDeleted(String property, Map params); + public void xmlPropertyDeleted(String property, Map params); -} \ No newline at end of file +} diff --git a/src/java/org/jivesoftware/util/TaskEngine.java b/src/java/org/jivesoftware/util/TaskEngine.java new file mode 100644 index 0000000..be5b747 --- /dev/null +++ b/src/java/org/jivesoftware/util/TaskEngine.java @@ -0,0 +1,302 @@ +/** + * $Revision: 4005 $ + * $Date: 2006-06-16 08:58:27 -0700 (Fri, 16 Jun 2006) $ + * + * Copyright (C) 2005-2008 Jive Software. All rights reserved. + * + * This software is published under the terms of the GNU Public License (GPL), + * a copy of which is included in this distribution, or a commercial license + * agreement with Jive. + */ + +package org.jivesoftware.util; + +import java.util.Date; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Performs tasks using worker threads. It also allows tasks to be scheduled to be + * run at future dates. This class mimics relevant methods in both + * {@link ExecutorService} and {@link Timer}. Any {@link TimerTask} that's + * scheduled to be run in the future will automatically be run using the thread + * executor's thread pool. This means that the standard restriction that TimerTasks + * should run quickly does not apply. + * + * @author Matt Tucker + */ +public class TaskEngine { + + private static TaskEngine instance = new TaskEngine(); + + /** + * Returns a task engine instance (singleton). + * + * @return a task engine. + */ + public static TaskEngine getInstance() { + return instance; + } + + private Timer timer; + private ExecutorService executor; + private Map wrappedTasks = new ConcurrentHashMap(); + + /** + * Constructs a new task engine. + */ + private TaskEngine() { + timer = new Timer("timer-openfire", true); + executor = Executors.newCachedThreadPool(new ThreadFactory() { + + final AtomicInteger threadNumber = new AtomicInteger(1); + + public Thread newThread(Runnable runnable) { + // Use our own naming scheme for the threads. + Thread thread = new Thread(Thread.currentThread().getThreadGroup(), runnable, + "pool-openfire" + threadNumber.getAndIncrement(), 0); + // Make workers daemon threads. + thread.setDaemon(true); + if (thread.getPriority() != Thread.NORM_PRIORITY) { + thread.setPriority(Thread.NORM_PRIORITY); + } + return thread; + } + }); + } + + /** + * Submits a Runnable task for execution and returns a Future + * representing that task. + * + * @param task the task to submit. + * @return a Future representing pending completion of the task, + * and whose get() method will return null + * upon completion. + * @throws java.util.concurrent.RejectedExecutionException if task cannot be scheduled + * for execution. + * @throws NullPointerException if task null. + */ + public Future submit(Runnable task) { + return executor.submit(task); + } + + /** + * Schedules the specified task for execution after the specified delay. + * + * @param task task to be scheduled. + * @param delay delay in milliseconds before task is to be executed. + * @throws IllegalArgumentException if delay is negative, or + * delay + System.currentTimeMillis() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, or timer was cancelled. + */ + public void schedule(TimerTask task, long delay) { + timer.schedule(new TimerTaskWrapper(task), delay); + } + + /** + * Schedules the specified task for execution at the specified time. If + * the time is in the past, the task is scheduled for immediate execution. + * + * @param task task to be scheduled. + * @param time time at which task is to be executed. + * @throws IllegalArgumentException if time.getTime() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, timer was cancelled, or timer thread terminated. + */ + public void schedule(TimerTask task, Date time) { + timer.schedule(new TimerTaskWrapper(task), time); + } + + /** + * Schedules the specified task for repeated fixed-delay execution, + * beginning after the specified delay. Subsequent executions take place + * at approximately regular intervals separated by the specified period. + * + *

In fixed-delay execution, each execution is scheduled relative to + * the actual execution time of the previous execution. If an execution + * is delayed for any reason (such as garbage collection or other + * background activity), subsequent executions will be delayed as well. + * In the long run, the frequency of execution will generally be slightly + * lower than the reciprocal of the specified period (assuming the system + * clock underlying Object.wait(long) is accurate). + * + *

Fixed-delay execution is appropriate for recurring activities + * that require "smoothness." In other words, it is appropriate for + * activities where it is more important to keep the frequency accurate + * in the short run than in the long run. This includes most animation + * tasks, such as blinking a cursor at regular intervals. It also includes + * tasks wherein regular activity is performed in response to human + * input, such as automatically repeating a character as long as a key + * is held down. + * + * @param task task to be scheduled. + * @param delay delay in milliseconds before task is to be executed. + * @param period time in milliseconds between successive task executions. + * @throws IllegalArgumentException if delay is negative, or + * delay + System.currentTimeMillis() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, timer was cancelled, or timer thread terminated. + */ + public void schedule(TimerTask task, long delay, long period) { + TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task); + wrappedTasks.put(task, taskWrapper); + timer.schedule(taskWrapper, delay, period); + } + + /** + * Schedules the specified task for repeated fixed-delay execution, + * beginning at the specified time. Subsequent executions take place at + * approximately regular intervals, separated by the specified period. + * + *

In fixed-delay execution, each execution is scheduled relative to + * the actual execution time of the previous execution. If an execution + * is delayed for any reason (such as garbage collection or other + * background activity), subsequent executions will be delayed as well. + * In the long run, the frequency of execution will generally be slightly + * lower than the reciprocal of the specified period (assuming the system + * clock underlying Object.wait(long) is accurate). + * + *

Fixed-delay execution is appropriate for recurring activities + * that require "smoothness." In other words, it is appropriate for + * activities where it is more important to keep the frequency accurate + * in the short run than in the long run. This includes most animation + * tasks, such as blinking a cursor at regular intervals. It also includes + * tasks wherein regular activity is performed in response to human + * input, such as automatically repeating a character as long as a key + * is held down. + * + * @param task task to be scheduled. + * @param firstTime First time at which task is to be executed. + * @param period time in milliseconds between successive task executions. + * @throws IllegalArgumentException if time.getTime() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, timer was cancelled, or timer thread terminated. + */ + public void schedule(TimerTask task, Date firstTime, long period) { + TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task); + wrappedTasks.put(task, taskWrapper); + timer.schedule(taskWrapper, firstTime, period); + } + + /** + * Schedules the specified task for repeated fixed-rate execution, + * beginning after the specified delay. Subsequent executions take place + * at approximately regular intervals, separated by the specified period. + * + *

In fixed-rate execution, each execution is scheduled relative to the + * scheduled execution time of the initial execution. If an execution is + * delayed for any reason (such as garbage collection or other background + * activity), two or more executions will occur in rapid succession to + * "catch up." In the long run, the frequency of execution will be + * exactly the reciprocal of the specified period (assuming the system + * clock underlying Object.wait(long) is accurate). + * + *

Fixed-rate execution is appropriate for recurring activities that + * are sensitive to absolute time, such as ringing a chime every + * hour on the hour, or running scheduled maintenance every day at a + * particular time. It is also appropriate for recurring activities + * where the total time to perform a fixed number of executions is + * important, such as a countdown timer that ticks once every second for + * ten seconds. Finally, fixed-rate execution is appropriate for + * scheduling multiple repeating timer tasks that must remain synchronized + * with respect to one another. + * + * @param task task to be scheduled. + * @param delay delay in milliseconds before task is to be executed. + * @param period time in milliseconds between successive task executions. + * @throws IllegalArgumentException if delay is negative, or + * delay + System.currentTimeMillis() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, timer was cancelled, or timer thread terminated. + */ + public void scheduleAtFixedRate(TimerTask task, long delay, long period) { + TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task); + wrappedTasks.put(task, taskWrapper); + timer.scheduleAtFixedRate(taskWrapper, delay, period); + } + + /** + * Schedules the specified task for repeated fixed-rate execution, + * beginning at the specified time. Subsequent executions take place at + * approximately regular intervals, separated by the specified period. + * + *

In fixed-rate execution, each execution is scheduled relative to the + * scheduled execution time of the initial execution. If an execution is + * delayed for any reason (such as garbage collection or other background + * activity), two or more executions will occur in rapid succession to + * "catch up." In the long run, the frequency of execution will be + * exactly the reciprocal of the specified period (assuming the system + * clock underlying Object.wait(long) is accurate). + * + *

Fixed-rate execution is appropriate for recurring activities that + * are sensitive to absolute time, such as ringing a chime every + * hour on the hour, or running scheduled maintenance every day at a + * particular time. It is also appropriate for recurring activities + * where the total time to perform a fixed number of executions is + * important, such as a countdown timer that ticks once every second for + * ten seconds. Finally, fixed-rate execution is appropriate for + * scheduling multiple repeating timer tasks that must remain synchronized + * with respect to one another. + * + * @param task task to be scheduled. + * @param firstTime First time at which task is to be executed. + * @param period time in milliseconds between successive task executions. + * @throws IllegalArgumentException if time.getTime() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, timer was cancelled, or timer thread terminated. + */ + public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { + TimerTaskWrapper taskWrapper = new TimerTaskWrapper(task); + wrappedTasks.put(task, taskWrapper); + timer.scheduleAtFixedRate(taskWrapper, firstTime, period); + } + + /** + * Cancels the execution of a scheduled task. {@link java.util.TimerTask#cancel()} + * + * @param task the scheduled task to cancel. + */ + public void cancelScheduledTask(TimerTask task) { + TaskEngine.TimerTaskWrapper taskWrapper = wrappedTasks.remove(task); + if (taskWrapper != null) { + taskWrapper.cancel(); + } + } + + /** + * Shuts down the task engine service. + */ + public void shutdown() { + if (executor != null) { + executor.shutdownNow(); + executor = null; + } + + if (timer != null) { + timer.cancel(); + timer = null; + } + } + + /** + * Wrapper class for a standard TimerTask. It simply executes the TimerTask + * using the executor's thread pool. + */ + private class TimerTaskWrapper extends TimerTask { + + private TimerTask task; + + public TimerTaskWrapper(TimerTask task) { + this.task = task; + } + + public void run() { + executor.submit(task); + } + } +} \ No newline at end of file diff --git a/test/org/jivesoftware/multiplexer/net/XMLLightweightParserTest.java b/test/org/jivesoftware/multiplexer/net/XMLLightweightParserTest.java index ab91fd7..91aa6a3 100644 --- a/test/org/jivesoftware/multiplexer/net/XMLLightweightParserTest.java +++ b/test/org/jivesoftware/multiplexer/net/XMLLightweightParserTest.java @@ -11,8 +11,8 @@ package org.jivesoftware.multiplexer.net; -import junit.framework.TestCase; import junit.framework.Assert; +import junit.framework.TestCase; import org.apache.mina.common.ByteBuffer; import org.dom4j.Element; import org.dom4j.io.SAXReader; @@ -298,6 +298,32 @@ } } + public void testInvalidSurrogates() throws Exception { + byte[] one = ("").getBytes(); + byte[] two = {(byte) 0xed, (byte) 0xb3, (byte) 0xb1}; + byte[] three = "".getBytes(); + + byte[] message = new byte[one.length + two.length + three.length]; + int j = 0; + for (byte b : one) { + message[j++] = b; + } + for (byte b : two) { + message[j++] = b; + } + for (byte b : three) { + message[j++] = b; + } + + ByteBuffer mybuffer = ByteBuffer.wrap(message); + try { + parser.read(mybuffer); + fail("Failed to detect a low surrogate char without a preceding high surrogate"); + } catch (Exception e) { + assertEquals("Incorrect exception was received", "Found low surrogate char without a preceding high surrogate", e.getMessage()); + } + } + public void testRead() { try { XMLLightweightParser parser = new XMLLightweightParser("UTF-8");