diff --git a/src/java/org/jivesoftware/multiplexer/ConnectionManager.java b/src/java/org/jivesoftware/multiplexer/ConnectionManager.java index ac94ea4..f3b6437 100644 --- a/src/java/org/jivesoftware/multiplexer/ConnectionManager.java +++ b/src/java/org/jivesoftware/multiplexer/ConnectionManager.java @@ -289,6 +289,12 @@ return port; } + public int getClientSSLListenerPort() { + if(JiveGlobals.getBooleanProperty("xmpp.socket.ssl.active", true)) + return JiveGlobals.getIntProperty("xmpp.socket.ssl.port", 5223); + return 0; + } + private void startClientListeners(String localIPAddress) { if (!JiveGlobals.getBooleanProperty("xmpp.socket.default.active", true)) { // Do not start listener if service is disabled diff --git a/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java b/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java index 38aa47d..f2b6468 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/BoshBindingError.java @@ -17,70 +17,70 @@ * 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), + badRequest(Type.terminate, "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"), + hostGone(Type.terminate, "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"), + hostUnknown(Type.terminate, "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"), + improperAddressing(Type.terminate, "improper-addressing"), /** * The connection manager has experienced an internal error that prevents it from servicing the * request. */ - internalServerError(Type.terminal, "internal-server-error"), + internalServerError(Type.terminate, "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), + itemNotFound(Type.terminate, "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"), + otherRequest(Type.terminate, "other-request"), /** * The client has broken the session rules (polling too frequently, requesting too frequently, * too many simultaneous requests). */ - policyViolation(Type.terminal, "policy-violation", + policyViolation(Type.terminate, "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"), + remoteConnectionFailed(Type.terminate, "remote-connection-failed"), /** * Encapsulates an error in the protocol being transported. */ - remoteStreamError(Type.terminal, "remote-stream-error"), + remoteStreamError(Type.terminate, "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"), + seeOtherUri(Type.terminate, "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"), + systemShutdown(Type.terminate, "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"); + undefinedCondition(Type.terminate, "undefined-condition"); private Type errorType; private String condition; @@ -123,10 +123,10 @@ public enum Type { /** - * The terminal error condition prevents the client from making any further requests until a + * The terminate error condition prevents the client from making any further requests until a * new session is established. */ - terminal(null), + terminate(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 diff --git a/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java b/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java index 7b42660..d7871a2 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/FlashCrossDomainServlet.java @@ -12,6 +12,7 @@ package org.jivesoftware.multiplexer.net.http; import org.jivesoftware.multiplexer.ConnectionManager; +import org.jivesoftware.util.JiveGlobals; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -27,23 +28,74 @@ */ public class FlashCrossDomainServlet extends HttpServlet { - public static String CROSS_DOMAIN_TEXT = "" + + private static String CROSS_DOMAIN_TEXT = "" + "" + "" + + "" + ""; - + private static String CROSS_DOMAIN_MIDDLE_TEXT = "\" secure=\""; + private static String CROSS_DOMAIN_END_TEXT = "\"/>"; + + private static String CROSS_DOMAIN_SECURE_ENABLED = "xmpp.httpbind.crossdomain.secure"; + private static boolean CROSS_DOMAIN_SECURE_DEFAULT = true; + @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(CROSS_DOMAIN_TEXT); + getPortList(builder); + builder.append(CROSS_DOMAIN_MIDDLE_TEXT); + getSecure(builder); + builder.append(CROSS_DOMAIN_END_TEXT); builder.append("\n"); response.setContentType("text/xml"); response.getOutputStream().write(builder.toString().getBytes()); } + + private StringBuilder getPortList(StringBuilder builder) { + boolean multiple = false; + if(ConnectionManager.getInstance().getClientListenerPort() > 0) { + builder.append(ConnectionManager.getInstance().getClientListenerPort()); + multiple = true; + } + if(ConnectionManager.getInstance().getClientSSLListenerPort() > 0) { + if(multiple) { + builder.append(","); + } + builder.append(ConnectionManager.getInstance().getClientSSLListenerPort()); + multiple = true; + } + + if(HttpBindManager.getInstance().isHttpBindEnabled()) { + // ports for http-binding may not be strictly needed in here, but it doesn't hurt + if(HttpBindManager.getInstance().getHttpBindUnsecurePort() > 0) { + if(multiple) { + builder.append(","); + } + builder.append(HttpBindManager.getInstance().getHttpBindUnsecurePort()); + multiple = true; + } + if(HttpBindManager.getInstance().getHttpBindSecurePort() > 0) { + if(multiple) { + builder.append(","); + } + builder.append(HttpBindManager.getInstance().getHttpBindSecurePort()); + } + } + + return builder; + } + + private StringBuilder getSecure(StringBuilder builder) { + if (JiveGlobals.getBooleanProperty(CROSS_DOMAIN_SECURE_ENABLED,CROSS_DOMAIN_SECURE_DEFAULT)) { + builder.append("true"); + } else { + builder.append("false"); + } + return builder; + } } + diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java index 33a240d..415b15b 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindException.java @@ -28,6 +28,6 @@ } public boolean shouldCloseSession() { - return error.getErrorType() == BoshBindingError.Type.terminal; + return error.getErrorType() == BoshBindingError.Type.terminate; } } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java index ff418e8..ea5bcd4 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpBindServlet.java @@ -13,6 +13,7 @@ import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserException; +import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; import org.jivesoftware.multiplexer.net.MXParser; import org.dom4j.io.XMPPPacketReader; @@ -20,6 +21,7 @@ import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.DocumentHelper; +import org.dom4j.QName; import org.mortbay.util.ajax.ContinuationSupport; import org.apache.commons.lang.StringEscapeUtils; @@ -33,6 +35,7 @@ import java.io.ByteArrayInputStream; import java.net.InetAddress; import java.net.URLDecoder; +import java.util.Date; /** * Servlet which handles requests to the HTTP binding service. It determines if there is currently @@ -97,9 +100,9 @@ sendLegacyError(response, BoshBindingError.badRequest); return; } - queryString = URLDecoder.decode(queryString, "utf-8"); + queryString = URLDecoder.decode(queryString, "UTF-8"); - parseDocument(request, response, new ByteArrayInputStream(queryString.getBytes())); + parseDocument(request, response, new ByteArrayInputStream(queryString.getBytes("UTF-8"))); } private void sendLegacyError(HttpServletResponse response, BoshBindingError error) @@ -173,8 +176,12 @@ BoshBindingError bindingError, HttpSession session) throws IOException { + if (JiveGlobals.getBooleanProperty("log.debug.enabled", false)) { + System.out.println(new Date()+": HTTP ERR("+session.getStreamID() + "): " + bindingError.getErrorType().getType() + ", " + bindingError.getCondition() + "."); + } try { - if (session.getVersion() >= 1.6) { + if ((session.getMajorVersion() == 1 && session.getMinorVersion() >= 6) || + session.getMajorVersion() > 1) { respond(response, createErrorBody(bindingError.getErrorType().getType(), bindingError.getCondition()), request.getMethod()); } @@ -183,7 +190,7 @@ } } finally { - if (bindingError.getErrorType() == BoshBindingError.Type.terminal) { + if (bindingError.getErrorType() == BoshBindingError.Type.terminate) { session.close(); } } @@ -201,6 +208,9 @@ HttpServletResponse response, Element rootNode) throws IOException { + if (JiveGlobals.getBooleanProperty("log.debug.enabled", false)) { + System.out.println(new Date()+": HTTP RECV(" + sid + "): " + rootNode.asXML()); + } long rid = getLongAttribue(rootNode.attributeValue("rid"), -1); if (rid <= 0) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Body missing RID (Request ID)"); @@ -230,11 +240,28 @@ } String type = rootNode.attributeValue("type"); + String restartStream = rootNode.attributeValue(new QName("restart", rootNode.getNamespaceForPrefix("xmpp"))); + int pauseDuration = getIntAttribue(rootNode.attributeValue("pause"), -1); + if ("terminate".equals(type)) { session.close(); respond(response, createEmptyBody(), request.getMethod()); } + else if ("true".equals(restartStream) && rootNode.elements().size() == 0) { + try { + respond(response, createSessionRestartResponse(session), request.getMethod()); + } + catch (DocumentException e) { + Log.error("Error sending session restart response to client.", e); + } + } + else if (pauseDuration > 0 && pauseDuration <= session.getMaxPause()) { + session.pause(pauseDuration); + respond(response, createEmptyBody(), request.getMethod()); + session.setLastResponseEmpty(true); + } else { + session.resetInactivityTimeout(); connection.setContinuation(ContinuationSupport.getContinuation(request, connection)); request.setAttribute("request-session", connection.getSession()); request.setAttribute("request", connection.getRequestId()); @@ -249,6 +276,22 @@ } } + private String createSessionRestartResponse(HttpSession session) throws DocumentException { + Element response = DocumentHelper.createElement("body"); + response.addNamespace("", "http://jabber.org/protocol/httpbind"); + response.addNamespace("stream", "http://etherx.jabber.org/streams"); + + Element features = response.addElement("stream:features"); + for (Element feature : session.getAvailableStreamFeaturesElements()) { + if (JiveGlobals.getBooleanProperty("log.debug.enabled", false)) { + System.out.println(new Date()+": Adding stream feature " + feature.asXML()); + } + features.add(feature); + } + + return response.asXML(); + } + private void createNewSession(HttpServletRequest request, HttpServletResponse response, Element rootNode) throws IOException @@ -263,6 +306,9 @@ HttpConnection connection = new HttpConnection(rid, request.isSecure()); InetAddress address = InetAddress.getByName(request.getRemoteAddr()); connection.setSession(sessionManager.createSession(address, rootNode, connection)); + if (JiveGlobals.getBooleanProperty("log.debug.enabled", false)) { + System.out.println(new Date()+": HTTP RECV(" + connection.getSession().getStreamID() + "): " + rootNode.asXML()); + } respond(response, connection, request.getMethod()); } catch (HttpBindException e) { @@ -280,6 +326,7 @@ } catch (HttpBindTimeoutException e) { content = createEmptyBody(); + connection.getSession().setLastResponseEmpty(true); } respond(response, content, method); @@ -292,9 +339,18 @@ response.setCharacterEncoding("utf-8"); if ("GET".equals(method)) { + if (JiveGlobals.getBooleanProperty("xmpp.httpbind.client.no-cache.enabled", true)) { + // Prevent caching of responses + response.addHeader("Cache-Control", "no-store"); + response.addHeader("Cache-Control", "no-cache"); + response.addHeader("Pragma", "no-cache"); + } content = "_BOSH_(\"" + StringEscapeUtils.escapeJavaScript(content) + "\")"; } + if (JiveGlobals.getBooleanProperty("log.debug.enabled", false)) { + System.out.println(new Date()+": HTTP SENT: " + content); + } byte[] byteContent = content.getBytes("utf-8"); response.setContentLength(byteContent.length); response.getOutputStream().write(byteContent); @@ -318,7 +374,19 @@ return defaultValue; } } - + + private int getIntAttribue(String value, int defaultValue) { + if (value == null || "".equals(value)) { + return defaultValue; + } + try { + return Integer.valueOf(value); + } + catch (Exception ex) { + return defaultValue; + } + } + private XMPPPacketReader getPacketReader() { // Reader is associated with a new XMPPPacketReader XMPPPacketReader reader = localReader.get(); diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java index ded3895..177e808 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpConnection.java @@ -12,6 +12,7 @@ package org.jivesoftware.multiplexer.net.http; +import org.jivesoftware.util.JiveConstants; import org.mortbay.util.ajax.Continuation; /** @@ -171,7 +172,7 @@ } private String waitForResponse() throws HttpBindTimeoutException { - if (continuation.suspend(session.getWait() * 1000)) { + if (continuation.suspend(session.getWait() * JiveConstants.SECOND)) { String deliverable = (String) continuation.getObject(); // This will occur when the hold attribute of a session has been exceded. this.isDelivered = true; diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java index 508453c..9e2a13c 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpSession.java @@ -18,7 +18,10 @@ import org.dom4j.io.XMPPPacketReader; import org.jivesoftware.multiplexer.ClientSession; import org.jivesoftware.multiplexer.ConnectionManager; +import org.jivesoftware.multiplexer.Session; import org.jivesoftware.multiplexer.net.MXParser; +import org.jivesoftware.util.JiveConstants; +import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -69,10 +72,14 @@ private Set listeners = new CopyOnWriteArraySet(); private volatile boolean isClosed; private int inactivityTimeout; + private int defaultInactivityTimeout; private long lastActivity; private long lastRequestID; + private boolean lastResponseEmpty; private int maxRequests; - private Double version = Double.NaN; + private int maxPause; + private int majorVersion = -1; + private int minorVersion = -1; // Semaphore which protects the packets to send, so, there can only be one consumer at a time. @@ -99,9 +106,11 @@ public Collection getAvailableStreamFeaturesElements() { List elements = new ArrayList(); - Element sasl = connectionManager.getServerSurrogate().getSASLMechanismsElement(this); - if (sasl != null) { - elements.add(sasl); + if (getStatus() != Session.STATUS_AUTHENTICATED) { + Element sasl = connectionManager.getServerSurrogate().getSASLMechanismsElement(this); + if (true || sasl != null) { + elements.add(sasl); + } } Element bind = DocumentHelper.createElement(new QName("bind", @@ -192,7 +201,7 @@ * * @param language the language this session is using. */ - public void setLanaguage(String language) { + public void setLanguage(String language) { this.language = language; } @@ -249,6 +258,28 @@ } /** + * Sets the maximum length of a temporary session pause (in seconds) that the client MAY + * request. + * + * @param maxPause the maximum length of a temporary session pause (in seconds) that the client + * MAY request. + */ + public void setMaxPause(int maxPause) { + this.maxPause = maxPause; + } + + /** + * Returns the maximum length of a temporary session pause (in seconds) that the client MAY + * request. + * + * @return the maximum length of a temporary session pause (in seconds) that the client MAY + * request. + */ + public int getMaxPause() { + return this.maxPause; + } + + /** * Returns true if all connections on this session should be secured, and false if they should * not. * @@ -260,6 +291,19 @@ } /** + * Returns true if this session is a polling session. Some clients may be restricted to open + * only one connection to the server. In this case the client SHOULD inform the server by + * setting the values of the 'wait' and/or 'hold' attributes in its session creation request + * to "0", and then "poll" the server at regular intervals throughout the session for stanzas + * it may have received from the server. + * + * @return true if this session is a polling session. + */ + public boolean isPollingSession() { + return (this.wait == 0 || this.hold == 0); + } + + /** * Adds a {@link SessionListener} to this session. The listener * will be notified of changes to the session. * @@ -280,6 +324,18 @@ } /** + * Sets the default inactivity timeout of this session. A session's inactivity timeout can + * be temporarily changed using session pause requests. + * + * @see #pause(int) + * + * @param defaultInactivityTimeout the default inactivity timeout of this session. + */ + public void setDefaultInactivityTimeout(int defaultInactivityTimeout) { + this.defaultInactivityTimeout = defaultInactivityTimeout; + } + + /** * Sets the time, in seconds, after which this session will be considered inactive and be be * terminated. * @@ -291,6 +347,16 @@ } /** + * Resets the inactivity timeout of this session to default. A session's inactivity timeout can + * be temporarily changed using session pause requests. + * + * @see #pause(int) + */ + public void resetInactivityTimeout() { + this.inactivityTimeout = this.defaultInactivityTimeout; + } + + /** * Returns the time, in seconds, after which this session will be considered inactive and * terminated. * @@ -302,6 +368,27 @@ } /** + * Pauses the session for the given amount of time. If a client encounters an exceptional + * temporary situation during which it will be unable to send requests to the connection + * manager for a period of time greater than the maximum inactivity period, then the client MAY + * request a temporary increase to the maximum inactivity period by including a 'pause' + * attribute in a request. + * + * @param duration the time, in seconds, after which this session will be considered inactive + * and terminated. + */ + public void pause(int duration) { + // Respond immediately to all pending requests + for (HttpConnection toClose : connectionQueue) { + if (!toClose.isClosed()) { + toClose.close(); + lastRequestID = toClose.getRequestId(); + } + } + setInactivityTimeout(duration); + } + + /** * 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. @@ -326,41 +413,101 @@ } /** - * 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. + * Returns the highest 'rid' attribute the server has received where it has also received + * all requests with lower 'rid' values. When responding to a request that it has been + * holding, if the server finds it has already received another request with a higher 'rid' + * attribute (typically while it was holding the first request), then it MAY acknowledge the + * reception to the client. * - * @param version the version of BOSH which the client implements, represented as a Double, - * {major version}.{minor version}. + * @return the highest 'rid' attribute the server has received where it has also received + * all requests with lower 'rid' values. */ - public void setVersion(double version) { - if(version <= 1.5) { - return; + public long getLastAcknowledged() { + long ack = lastRequestID; + Collections.sort(connectionQueue, connectionComparator); + for (HttpConnection connection : connectionQueue) { + if (connection.getRequestId() == ack + 1) { + ack++; + } } - else if(version >= 1.6) { - version = 1.6; - } - this.version = version; + return ack; } /** - * Returns the BOSH version which this session utilizes. The version refers to the + * Sets the major version of BOSH which the client implements. Currently, the only versions + * supported by Openfire are 1.5 and 1.6. + * + * @param majorVersion the major version of BOSH which the client implements. + */ + public void setMajorVersion(int majorVersion) { + if(majorVersion != 1) { + return; + } + this.majorVersion = majorVersion; + } + + /** + * Returns the major version of BOSH 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. + * @return the major version of the BOSH XEP which the client is utilizing. */ - public double getVersion() { - if (!Double.isNaN(this.version)) { - return this.version; + public int getMajorVersion() { + if (this.majorVersion != -1) { + return this.majorVersion; } else { - return 1.5; + return 1; } } + /** + * Sets the minor 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 5 will be + * interpreted as 5 and any values greater than or equal to 6 will be interpreted as 6. + * + * @param minorVersion the minor version of BOSH which the client implements. + */ + public void setMinorVersion(int minorVersion) { + if(minorVersion <= 5) { + this.minorVersion = 5; + } + else if(minorVersion >= 6) { + this.minorVersion = 6; + } + } + + /** + * Returns the major version of BOSH 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 5 is returned as 1.5 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 minor version of the BOSH XEP which the client is utilizing. + */ + public int getMinorVersion() { + if (this.minorVersion != -1) { + return this.minorVersion; + } + else { + return 5; + } + } + + /** + * lastResponseEmpty true if last response of this session is an empty body element. This + * is used in overactivity checking. + * + * @param lastResponseEmpty true if last response of this session is an empty body element. + */ + public void setLastResponseEmpty(boolean lastResponseEmpty) { + this.lastResponseEmpty = lastResponseEmpty; + } + public String getResponse(long requestID) throws HttpBindException { for (HttpConnection connection : connectionQueue) { if (connection.getRequestId() == requestID) { @@ -392,6 +539,7 @@ } if (response == null) { response = createEmptyBody(); + setLastResponseEmpty(true); } return response; } @@ -422,7 +570,7 @@ * protocol. */ synchronized HttpConnection createConnection(long rid, Collection packetsToBeSent, - boolean isSecure) + boolean isSecure, boolean isPoll) throws HttpConnectionClosedException, HttpBindException { HttpConnection connection = new HttpConnection(rid, isSecure); @@ -436,14 +584,13 @@ 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."); + else if (rid > (lastRequestID + maxRequests)) { + Log.warn("Request " + rid + " > " + (lastRequestID + maxRequests) + ", ending session."); throw new HttpBindException("Unexpected RID error.", BoshBindingError.itemNotFound); } - addConnection(connection, packetsToBeSent.size() <= 0); + addConnection(connection, isPoll); return connection; } @@ -462,9 +609,7 @@ throw new IllegalArgumentException("Connection cannot be null."); } - if (isPoll) { - checkPollingInterval(); - } + checkOveractivity(isPoll); if (isSecure && !connection.isSecure()) { throw new HttpBindException("Session was started from secure connection, all " + @@ -474,30 +619,47 @@ 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)) { + if (isPollingSession() || (pendingElements.size() > 0 && connection.getRequestId() == lastRequestID + 1)) { deliver(connection, pendingElements); lastRequestID = connection.getRequestId(); pendingElements.clear(); + connectionQueue.add(connection); + Collections.sort(connectionQueue, connectionComparator); } 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; + connectionQueue.add(connection); + Collections.sort(connectionQueue, connectionComparator); + + int connectionsToClose; + if(connectionQueue.get(connectionQueue.size() - 1) != connection) { + // Current connection does not have the greatest rid. That means + // requests were received out of order, respond to all. + connectionsToClose = connectionQueue.size(); + } + else { + // Everything's fine, number of current connections open tells us + // how many that we need to close. + connectionsToClose = getOpenConnectionCount() - hold; + } int closed = 0; for (int i = 0; i < connectionQueue.size() && closed < connectionsToClose; i++) { HttpConnection toClose = connectionQueue.get(i); - if (!toClose.isClosed()) { + if (!toClose.isClosed() && toClose.getRequestId() == lastRequestID + 1) { + if(toClose == connection) { + // Current connection has no continuation yet, just deliver. + deliver(new Deliverable("")); + } + else { + toClose.close(); + } lastRequestID = toClose.getRequestId(); - toClose.close(); closed++; } } } - connectionQueue.add(connection); - Collections.sort(connectionQueue, connectionComparator); fireConnectionOpened(connection); } @@ -531,17 +693,72 @@ } } - 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); + /** + * Check that the client SHOULD NOT make more simultaneous requests than specified + * by the 'requests' attribute in the connection manager's Session Creation Response. + * However the client MAY make one additional request if it is to pause or terminate a session. + * + * @see overactive. + * @param isPoll true if the session is using polling. + * @throws HttpBindException if the connection has violated a facet of the HTTP binding + * protocol. + */ + private void checkOveractivity(boolean isPoll) throws HttpBindException { + int pendingConnections = 0; + boolean overactivity = false; + String errorMessage = "Overactivity detected"; + + for (HttpConnection conn : connectionQueue) { + if (!conn.isClosed()) { + pendingConnections++; + } } - lastPoll = time; + + if(pendingConnections >= maxRequests) { + overactivity = true; + errorMessage += ", too many simultaneous requests."; + } + else if(isPoll) { + long time = System.currentTimeMillis(); + if (time - lastPoll < maxPollingInterval * JiveConstants.SECOND) { + if(isPollingSession()) { + overactivity = lastResponseEmpty; + } + else { + overactivity = (pendingConnections >= maxRequests - 1); + } + } + errorMessage += ", minimum polling interval is " + + maxPollingInterval + ", current interval " + ((time - lastPoll) / 1000); + lastPoll = time; + } + setLastResponseEmpty(false); + + if(overactivity) { + Log.debug(errorMessage); + if (!JiveGlobals.getBooleanProperty("xmpp.httpbind.client.requests.ignoreOveractivity", false)) { + throw new HttpBindException(errorMessage, BoshBindingError.policyViolation); + } + } } public void deliver(Element stanza) { + // Until session is not authenticated we need to inspect server traffic + if (status != Session.STATUS_AUTHENTICATED) { + String tag = stanza.getName(); + if ("success".equals(tag)) { + // Session has been authenticated (using SASL). Update status + setStatus(Session.STATUS_AUTHENTICATED); + } + else if ("failure".equals(tag)) { + // Sasl authentication has failed + // Ignore for now + } + else if ("challenge".equals(tag)) { + // A challenge was sent to the client. Client needs to respond + // Ignore for now + } + } deliver(new Deliverable(Arrays.asList(stanza))); } @@ -576,7 +793,15 @@ private String createDeliverable(Collection elements) { StringBuilder builder = new StringBuilder(); - builder.append(""); + builder.append(" lastRequestID) + builder.append(" ack='").append(ack).append("'"); + + builder.append(">"); + + setLastResponseEmpty(elements.size() == 0); for (Deliverable child : elements) { builder.append(child.getDeliverable()); } @@ -655,10 +880,12 @@ } } - - private static String createEmptyBody() { + private String createEmptyBody() { Element body = DocumentHelper.createElement("body"); body.addNamespace("", "http://jabber.org/protocol/httpbind"); + long ack = getLastAcknowledged(); + if(ack > lastRequestID) + body.addAttribute("ack", String.valueOf(ack)); return body.asXML(); } diff --git a/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java b/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java index c85a4e0..cc9176d 100644 --- a/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java +++ b/src/java/org/jivesoftware/multiplexer/net/http/HttpSessionManager.java @@ -14,6 +14,7 @@ import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.Element; +import org.dom4j.QName; import org.jivesoftware.multiplexer.ConnectionManager; import org.jivesoftware.multiplexer.ServerSurrogate; import org.jivesoftware.multiplexer.Session; @@ -118,18 +119,35 @@ int wait = getIntAttribute(rootNode.attributeValue("wait"), 60); int hold = getIntAttribute(rootNode.attributeValue("hold"), 1); - double version = getDoubleAttribute(rootNode.attributeValue("ver"), 1.5); + String version = rootNode.attributeValue("ver"); + if (version == null || "".equals(version)) { + version = "1.5"; + } + HttpSession session = createSession(connection.getRequestId(), address); session.setWait(Math.min(wait, getMaxWait())); session.setHold(hold); session.setSecure(connection.isSecure()); session.setMaxPollingInterval(getPollingInterval()); session.setMaxRequests(getMaxRequests()); - session.setInactivityTimeout(getInactivityTimeout()); + session.setMaxPause(getMaxPause()); + + if(session.isPollingSession()) { + session.setDefaultInactivityTimeout(getPollingInactivityTimeout()); + } + else { + session.setDefaultInactivityTimeout(getInactivityTimeout()); + } + session.resetInactivityTimeout(); + // Store language and version information in the connection. - session.setLanaguage(language); - session.setVersion(version); + session.setLanguage(language); + + String [] versionString = version.split("\\."); + session.setMajorVersion(Integer.parseInt(versionString[0])); + session.setMinorVersion(Integer.parseInt(versionString[1])); + try { connection.deliverBody(createSessionCreationResponse(session)); } @@ -144,6 +162,16 @@ return session; } + /** + * Returns the maximum length of a temporary session pause (in seconds) that the client MAY + * request. + * + * @return the maximum length of a temporary session pause (in seconds) that the client MAY + * request. + */ + public int getMaxPause() { + return JiveGlobals.getIntProperty("xmpp.httpbind.client.maxpause", 300); + } /** * Returns the longest time (in seconds) that Openfire is allowed to wait before responding to @@ -187,7 +215,7 @@ } /** - * Seconds a session has to be idle to be closed. Default is 30 minutes. Sending stanzas to the + * Seconds a session has to be idle to be closed. Default is 30. 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 @@ -201,6 +229,20 @@ } /** + * Seconds a polling session has to be idle to be closed. Default is 60. 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 polling session has to be idle to be closed. + */ + public int getPollingInactivityTimeout() { + return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle.polling", 60); + } + + /** * 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 @@ -222,8 +264,14 @@ HttpConnectionClosedException { //noinspection unchecked - List elements = rootNode.elements(); - HttpConnection connection = session.createConnection(rid, elements, isSecure); + List elements = rootNode.elements();boolean isPoll = (elements.size() == 0); + if ("terminate".equals(rootNode.attributeValue("type"))) + isPoll = false; + else if ("true".equals(rootNode.attributeValue(new QName("restart", rootNode.getNamespaceForPrefix("xmpp"))))) + isPoll = false; + else if (rootNode.attributeValue("pause") != null) + isPoll = false; + HttpConnection connection = session.createConnection(rid, elements, isSecure, isPoll); for (Element packet : elements) { serverSurrogate.send(packet.asXML(), session.getStreamID()); } @@ -279,8 +327,13 @@ response.addAttribute("inactivity", String.valueOf(session.getInactivityTimeout())); 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())); + if ((session.getMajorVersion() == 1 && session.getMinorVersion() >= 6) || + session.getMajorVersion() > 1) { + response.addAttribute("hold", String.valueOf(session.getHold())); + response.addAttribute("ack", String.valueOf(session.getLastAcknowledged())); + response.addAttribute("maxpause", String.valueOf(session.getMaxPause())); + response.addAttribute("ver", String.valueOf(session.getMajorVersion()) + + "." + String.valueOf(session.getMinorVersion())); } Element features = response.addElement("stream:features"); @@ -296,8 +349,8 @@ public void run() { long currentTime = System.currentTimeMillis(); for (HttpSession session : sessionMap.values()) { - long lastActive = (currentTime - session.getLastActivity()) / 1000; - if (lastActive > session.getInactivityTimeout()) { + long lastActive = currentTime - session.getLastActivity(); + if (lastActive > session.getInactivityTimeout() * JiveConstants.SECOND) { session.close(); } } diff --git a/src/java/org/jivesoftware/util/CertificateManager.java b/src/java/org/jivesoftware/util/CertificateManager.java index 92e7d90..61bf6ad 100644 --- a/src/java/org/jivesoftware/util/CertificateManager.java +++ b/src/java/org/jivesoftware/util/CertificateManager.java @@ -286,7 +286,7 @@ for (Enumeration aliases = ksKeys.aliases(); aliases.hasMoreElements();) { X509Certificate certificate = (X509Certificate) ksKeys.getCertificate(aliases.nextElement()); for (String identity : getPeerIdentities(certificate)) { - if (identity.endsWith(domain) && certificate.getPublicKey().getAlgorithm().equals(algorithm)) { + if (("*".equals(domain) || identity.endsWith(domain)) && certificate.getPublicKey().getAlgorithm().equals(algorithm)) { return true; } }