Commit 59998a2f authored by Alex Wenckus's avatar Alex Wenckus Committed by alex

Work on version 1.6 of BOSH. JM-1033

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@8043 b35dd754-fafc-0310-a699-88a17e54d16e
parent feda358a
/**
* $Revision:$
* $Date:$
*
* Copyright (C) 2007 Jive Software. All rights reserved.
* This software is the proprietary information of Jive Software. Use is subject to license terms.
*/
package org.jivesoftware.openfire.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;
}
}
}
}
...@@ -15,20 +15,18 @@ package org.jivesoftware.openfire.http; ...@@ -15,20 +15,18 @@ package org.jivesoftware.openfire.http;
* *
*/ */
public class HttpBindException extends Exception { public class HttpBindException extends Exception {
private boolean shouldCloseSession; private BoshBindingError error;
private int httpError;
public HttpBindException(String message, boolean shouldCloseSession, int httpError) { public HttpBindException(String message, BoshBindingError error) {
super(message); super(message);
this.shouldCloseSession = shouldCloseSession; this.error = error;
this.httpError = httpError;
} }
public int getHttpError() { public BoshBindingError getBindingError() {
return httpError; return error;
} }
public boolean shouldCloseSession() { public boolean shouldCloseSession() {
return shouldCloseSession; return error.getErrorType() == BoshBindingError.Type.terminal;
} }
} }
...@@ -13,6 +13,7 @@ package org.jivesoftware.openfire.http; ...@@ -13,6 +13,7 @@ package org.jivesoftware.openfire.http;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.jivesoftware.util.Log; import org.jivesoftware.util.Log;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.openfire.net.MXParser; import org.jivesoftware.openfire.net.MXParser;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.dom4j.io.XMPPPacketReader; import org.dom4j.io.XMPPPacketReader;
...@@ -80,13 +81,19 @@ public class HttpBindServlet extends HttpServlet { ...@@ -80,13 +81,19 @@ public class HttpBindServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException throws ServletException, IOException
{ {
boolean isScriptSyntaxEnabled =
JiveGlobals.getBooleanProperty("xmpp.httpbind.scriptSyntax.enabled", false);
if(!isScriptSyntaxEnabled) {
sendLegacyError(response, BoshBindingError.itemNotFound);
return;
}
if (isContinuation(request, response)) { if (isContinuation(request, response)) {
return; return;
} }
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString == null || "".equals(queryString)) { if (queryString == null || "".equals(queryString)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, sendLegacyError(response, BoshBindingError.badRequest);
"Unable to parse request content");
return; return;
} }
queryString = URLDecoder.decode(queryString, "utf-8"); queryString = URLDecoder.decode(queryString, "utf-8");
...@@ -94,6 +101,12 @@ public class HttpBindServlet extends HttpServlet { ...@@ -94,6 +101,12 @@ public class HttpBindServlet extends HttpServlet {
parseDocument(request, response, new ByteArrayInputStream(queryString.getBytes())); parseDocument(request, response, new ByteArrayInputStream(queryString.getBytes()));
} }
private void sendLegacyError(HttpServletResponse response, BoshBindingError error)
throws IOException
{
response.sendError(error.getLegacyErrorCode());
}
@Override @Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) protected void doPost(HttpServletRequest request, HttpServletResponse response)
...@@ -115,16 +128,14 @@ public class HttpBindServlet extends HttpServlet { ...@@ -115,16 +128,14 @@ public class HttpBindServlet extends HttpServlet {
} }
catch (Exception e) { catch (Exception e) {
Log.warn("Error parsing user request. [" + request.getRemoteAddr() + "]"); Log.warn("Error parsing user request. [" + request.getRemoteAddr() + "]");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, sendLegacyError(response, BoshBindingError.badRequest);
"Unable to parse request content: " + e.getMessage());
return; return;
} }
Element node = document.getRootElement(); Element node = document.getRootElement();
if (node == null || !"body".equals(node.getName())) { if (node == null || !"body".equals(node.getName())) {
Log.warn("Body missing from request content. [" + request.getRemoteAddr() + "]"); Log.warn("Body missing from request content. [" + request.getRemoteAddr() + "]");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, sendLegacyError(response, BoshBindingError.badRequest);
"Body missing from request content.");
return; return;
} }
...@@ -151,15 +162,40 @@ public class HttpBindServlet extends HttpServlet { ...@@ -151,15 +162,40 @@ public class HttpBindServlet extends HttpServlet {
request.getMethod()); request.getMethod());
} }
catch (HttpBindException e) { catch (HttpBindException e) {
response.sendError(e.getHttpError(), e.getMessage()); sendError(request, response, e.getBindingError(), session);
if (e.shouldCloseSession()) {
session.close();
}
} }
} }
return true; 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, private void handleSessionRequest(String sid, HttpServletRequest request,
HttpServletResponse response, Element rootNode) HttpServletResponse response, Element rootNode)
throws IOException throws IOException
...@@ -184,10 +220,7 @@ public class HttpBindServlet extends HttpServlet { ...@@ -184,10 +220,7 @@ public class HttpBindServlet extends HttpServlet {
request.isSecure(), rootNode); request.isSecure(), rootNode);
} }
catch (HttpBindException e) { catch (HttpBindException e) {
response.sendError(e.getHttpError(), e.getMessage()); sendError(request, response, e.getBindingError(), session);
if (e.shouldCloseSession()) {
session.close();
}
return; return;
} }
catch (HttpConnectionClosedException nc) { catch (HttpConnectionClosedException nc) {
...@@ -210,10 +243,7 @@ public class HttpBindServlet extends HttpServlet { ...@@ -210,10 +243,7 @@ public class HttpBindServlet extends HttpServlet {
request.getMethod()); request.getMethod());
} }
catch (HttpBindException e) { catch (HttpBindException e) {
response.sendError(e.getHttpError(), e.getMessage()); sendError(request, response, e.getBindingError(), session);
if (e.shouldCloseSession()) {
session.close();
}
} }
} }
} }
......
...@@ -56,6 +56,7 @@ public class HttpSession extends ClientSession { ...@@ -56,6 +56,7 @@ public class HttpSession extends ClientSession {
private long lastRequestID; private long lastRequestID;
private int maxRequests; private int maxRequests;
private PacketDeliverer backupDeliverer; private PacketDeliverer backupDeliverer;
private Double version = Double.NaN;
private final Queue<Collection<Element>> packetsToSend = new LinkedList<Collection<Element>>(); private final Queue<Collection<Element>> packetsToSend = new LinkedList<Collection<Element>>();
// Semaphore which protects the packets to send, so, there can only be one consumer at a time. // Semaphore which protects the packets to send, so, there can only be one consumer at a time.
...@@ -324,6 +325,42 @@ public class HttpSession extends ClientSession { ...@@ -324,6 +325,42 @@ public class HttpSession extends ClientSession {
} }
} }
/**
* 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 <a
* href="http://www.xmpp.org/extensions/xep-0124.html">XEP</a> 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 (this.version != Double.NaN) {
return this.version;
}
else {
return 1.5;
}
}
public String getResponse(long requestID) throws HttpBindException { public String getResponse(long requestID) throws HttpBindException {
for (HttpConnection connection : connectionQueue) { for (HttpConnection connection : connectionQueue) {
if (connection.getRequestId() == requestID) { if (connection.getRequestId() == requestID) {
...@@ -348,7 +385,8 @@ public class HttpSession extends ClientSession { ...@@ -348,7 +385,8 @@ public class HttpSession extends ClientSession {
catch (HttpBindTimeoutException e) { catch (HttpBindTimeoutException e) {
// This connection timed out we need to increment the request count // This connection timed out we need to increment the request count
if (connection.getRequestId() != lastRequestID + 1) { if (connection.getRequestId() != lastRequestID + 1) {
throw new HttpBindException("Unexpected RID error.", true, 404); throw new HttpBindException("Unexpected RID error.",
BoshBindingError.itemNotFound);
} }
lastRequestID = connection.getRequestId(); lastRequestID = connection.getRequestId();
} }
...@@ -427,7 +465,8 @@ public class HttpSession extends ClientSession { ...@@ -427,7 +465,8 @@ public class HttpSession extends ClientSession {
Delivered deliverable = retrieveDeliverable(rid); Delivered deliverable = retrieveDeliverable(rid);
if (deliverable == null) { if (deliverable == null) {
Log.warn("Deliverable unavailable for " + rid); Log.warn("Deliverable unavailable for " + rid);
throw new HttpBindException("Unexpected RID Error", true, 404); throw new HttpBindException("Unexpected RID error.",
BoshBindingError.itemNotFound);
} }
connection.deliverBody(createDeliverable(deliverable.deliverables)); connection.deliverBody(createDeliverable(deliverable.deliverables));
return connection; return connection;
...@@ -435,7 +474,8 @@ public class HttpSession extends ClientSession { ...@@ -435,7 +474,8 @@ public class HttpSession extends ClientSession {
else if (rid > (lastRequestID + hold + 1)) { else if (rid > (lastRequestID + hold + 1)) {
// TODO handle the case of greater RID which basically has it wait // TODO handle the case of greater RID which basically has it wait
Log.warn("Request " + rid + " > " + (lastRequestID + hold + 1) + ", ending session."); Log.warn("Request " + rid + " > " + (lastRequestID + hold + 1) + ", ending session.");
throw new HttpBindException("Unexpected RID Error", true, 404); throw new HttpBindException("Unexpected RID error.",
BoshBindingError.itemNotFound);
} }
if (packetsToBeSent.size() > 0) { if (packetsToBeSent.size() > 0) {
...@@ -466,7 +506,7 @@ public class HttpSession extends ClientSession { ...@@ -466,7 +506,7 @@ public class HttpSession extends ClientSession {
if (isSecure && !connection.isSecure()) { if (isSecure && !connection.isSecure()) {
throw new HttpBindException("Session was started from secure connection, all " + throw new HttpBindException("Session was started from secure connection, all " +
"connections on this session must be secured.", false, 403); "connections on this session must be secured.", BoshBindingError.badRequest);
} }
connection.setSession(this); connection.setSession(this);
...@@ -540,7 +580,7 @@ public class HttpSession extends ClientSession { ...@@ -540,7 +580,7 @@ public class HttpSession extends ClientSession {
if (((time - lastPoll) / 1000) < maxPollingInterval) { if (((time - lastPoll) / 1000) < maxPollingInterval) {
throw new HttpBindException("Too frequent polling minimum interval is " throw new HttpBindException("Too frequent polling minimum interval is "
+ maxPollingInterval + ", current interval " + ((time - lastPoll) / 1000), + maxPollingInterval + ", current interval " + ((time - lastPoll) / 1000),
true, 403); BoshBindingError.policyViolation);
} }
lastPoll = time; lastPoll = time;
} }
......
...@@ -16,8 +16,6 @@ import org.jivesoftware.util.JiveConstants; ...@@ -16,8 +16,6 @@ import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.TaskEngine; import org.jivesoftware.util.TaskEngine;
import org.jivesoftware.openfire.SessionManager; import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID; import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.SessionPacketRouter;
import org.jivesoftware.openfire.multiplex.UnknownStanzaException;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.dom4j.*; import org.dom4j.*;
...@@ -25,8 +23,6 @@ import java.util.*; ...@@ -25,8 +23,6 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress; import java.net.InetAddress;
/** /**
...@@ -116,6 +112,7 @@ public class HttpSessionManager { ...@@ -116,6 +112,7 @@ public class HttpSessionManager {
int wait = getIntAttribute(rootNode.attributeValue("wait"), 60); int wait = getIntAttribute(rootNode.attributeValue("wait"), 60);
int hold = getIntAttribute(rootNode.attributeValue("hold"), 1); int hold = getIntAttribute(rootNode.attributeValue("hold"), 1);
double version = getDoubleAttribute(rootNode.attributeValue("ver"), 1.5);
HttpSession session = createSession(connection.getRequestId(), address); HttpSession session = createSession(connection.getRequestId(), address);
session.setWait(Math.min(wait, getMaxWait())); session.setWait(Math.min(wait, getMaxWait()));
...@@ -126,6 +123,7 @@ public class HttpSessionManager { ...@@ -126,6 +123,7 @@ public class HttpSessionManager {
session.setInactivityTimeout(getInactivityTimeout()); session.setInactivityTimeout(getInactivityTimeout());
// Store language and version information in the connection. // Store language and version information in the connection.
session.setLanaguage(language); session.setLanaguage(language);
session.setVersion(version);
try { try {
connection.deliverBody(createSessionCreationResponse(session)); connection.deliverBody(createSessionCreationResponse(session));
} }
...@@ -134,7 +132,8 @@ public class HttpSessionManager { ...@@ -134,7 +132,8 @@ public class HttpSessionManager {
} }
catch (DocumentException e) { catch (DocumentException e) {
Log.error("Error creating document", e); Log.error("Error creating document", e);
throw new HttpBindException("Internal server error", true, 500); throw new HttpBindException("Internal server error",
BoshBindingError.internalServerError);
} }
return session; return session;
} }
...@@ -238,7 +237,7 @@ public class HttpSessionManager { ...@@ -238,7 +237,7 @@ public class HttpSessionManager {
} }
private static int getIntAttribute(String value, int defaultValue) { private static int getIntAttribute(String value, int defaultValue) {
if (value == null || "".equals(value)) { if (value == null || "".equals(value.trim())) {
return defaultValue; return defaultValue;
} }
try { try {
...@@ -249,6 +248,18 @@ public class HttpSessionManager { ...@@ -249,6 +248,18 @@ public class HttpSessionManager {
} }
} }
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 { private String createSessionCreationResponse(HttpSession session) throws DocumentException {
Element response = DocumentHelper.createElement("body"); Element response = DocumentHelper.createElement("body");
response.addNamespace("", "http://jabber.org/protocol/httpbind"); response.addNamespace("", "http://jabber.org/protocol/httpbind");
...@@ -260,6 +271,9 @@ public class HttpSessionManager { ...@@ -260,6 +271,9 @@ public class HttpSessionManager {
response.addAttribute("inactivity", String.valueOf(session.getInactivityTimeout())); response.addAttribute("inactivity", String.valueOf(session.getInactivityTimeout()));
response.addAttribute("polling", String.valueOf(session.getMaxPollingInterval())); response.addAttribute("polling", String.valueOf(session.getMaxPollingInterval()));
response.addAttribute("wait", String.valueOf(session.getWait())); 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"); Element features = response.addElement("stream:features");
for (Element feature : session.getAvailableStreamFeaturesElements()) { for (Element feature : session.getAvailableStreamFeaturesElements()) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment