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;
}
}