org.jivesoftware.openfire.http.HttpSession Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.http;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.dom4j.*;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.multiplex.UnknownStanzaException;
import org.jivesoftware.openfire.net.MXParser;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.VirtualConnection;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.spi.ConnectionManagerImpl;
import org.jivesoftware.openfire.spi.ConnectionType;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.TaskEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
/**
* A session represents a series of interactions with an XMPP client sending packets using the HTTP
* Binding protocol specified in XEP-0124.
* A session can have several client connections open simultaneously while awaiting packets bound
* for the client from the server.
*
* @author Alexander Wenckus
*/
public class HttpSession extends LocalClientSession {
private static final Logger Log = LoggerFactory.getLogger(HttpSession.class);
private static XmlPullParserFactory factory = null;
private static ThreadLocal localParser = null;
static {
try {
factory = XmlPullParserFactory.newInstance(MXParser.class.getName(), null);
factory.setNamespaceAware(true);
}
catch (XmlPullParserException e) {
Log.error("Error creating a parser factory", e);
}
// Create xmpp parser to keep in each thread
localParser = new ThreadLocal() {
@Override
protected XMPPPacketReader initialValue() {
XMPPPacketReader parser = new XMPPPacketReader();
factory.setNamespaceAware(true);
parser.setXPPFactory(factory);
return parser;
}
};
}
private int wait;
private int hold = 0;
private String language;
private final List connectionQueue = Collections.synchronizedList(new LinkedList());
private final List pendingElements = Collections.synchronizedList(new ArrayList());
private final List sentElements = Collections.synchronizedList(new ArrayList());
private boolean isSecure;
private int maxPollingInterval;
private long lastPoll = -1;
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 int maxPause;
private PacketDeliverer backupDeliverer;
private int majorVersion = -1;
private int minorVersion = -1;
private X509Certificate[] sslCertificates;
private final Queue> packetsToSend = new LinkedList<>();
// Semaphore which protects the packets to send, so, there can only be one consumer at a time.
private SessionPacketRouter router;
private static final Comparator connectionComparator
= new Comparator() {
@Override
public int compare(HttpConnection o1, HttpConnection o2) {
return (int) (o1.getRequestId() - o2.getRequestId());
}
};
public HttpSession(PacketDeliverer backupDeliverer, String serverName, InetAddress address,
StreamID streamID, long rid, HttpConnection connection, Locale language) {
super(serverName, new HttpVirtualConnection(address, ConnectionType.SOCKET_C2S), streamID, language);
this.isClosed = false;
this.lastActivity = System.currentTimeMillis();
this.lastRequestID = rid;
this.backupDeliverer = backupDeliverer;
this.sslCertificates = connection.getPeerCertificates();
}
/**
* Returns the stream features which are available for this session.
*
* @return the stream features which are available for this session.
*/
public Collection getAvailableStreamFeaturesElements() {
List elements = new ArrayList<>();
if (getAuthToken() == null) {
Element sasl = SASLAuthentication.getSASLMechanismsElement(this);
if (sasl != null) {
elements.add(sasl);
}
}
if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
elements.add(DocumentHelper.createElement(new QName("register",
new Namespace("", "http://jabber.org/features/iq-register"))));
}
Element bind = DocumentHelper.createElement(new QName("bind",
new Namespace("", "urn:ietf:params:xml:ns:xmpp-bind")));
elements.add(bind);
Element session = DocumentHelper.createElement(new QName("session",
new Namespace("", "urn:ietf:params:xml:ns:xmpp-session")));
session.addElement("optional");
elements.add(session);
return elements;
}
@Override
public String getAvailableStreamFeatures() {
StringBuilder sb = new StringBuilder(200);
for (Element element : getAvailableStreamFeaturesElements()) {
sb.append(element.asXML());
}
return sb.toString();
}
/**
* Closes the session. After a session has been closed it will no longer accept new connections
* on the session ID.
*/
@Override
public void close() {
if (isClosed) {
return;
}
conn.close();
}
/**
* Returns true if this session has been closed and no longer actively accepting connections.
*
* @return true if this session has been closed and no longer actively accepting connections.
*/
@Override
public boolean isClosed() {
return isClosed;
}
/**
* Specifies the longest time (in seconds) that the connection manager is allowed to wait before
* responding to any request during the session. This enables the client to prevent its TCP
* connection from expiring due to inactivity, as well as to limit the delay before it discovers
* any network failure.
*
* @param wait the longest time it is permissible to wait for a response.
*/
public void setWait(int wait) {
this.wait = wait;
}
/**
* Specifies the longest time (in seconds) that the connection manager is allowed to wait before
* responding to any request during the session. This enables the client to prevent its TCP
* connection from expiring due to inactivity, as well as to limit the delay before it discovers
* any network failure.
*
* @return the longest time it is permissible to wait for a response.
*/
public int getWait() {
return wait;
}
/**
* Specifies the maximum number of requests the connection manager is allowed to keep waiting at
* any one time during the session. (For example, if a constrained client is unable to keep open
* more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify
* a value of "1".)
*
* @param hold the maximum number of simultaneous waiting requests.
*/
public void setHold(int hold) {
this.hold = hold;
}
/**
* Specifies the maximum number of requests the connection manager is allowed to keep waiting at
* any one time during the session. (For example, if a constrained client is unable to keep open
* more than two HTTP connections to the same HTTP server simultaneously, then it SHOULD specify
* a value of "1".)
*
* @return the maximum number of simultaneous waiting requests
*/
public int getHold() {
return hold;
}
/**
* Sets the max interval within which a client can send polling requests. If more than one
* request occurs in the interval the session will be terminated.
*
* @param maxPollingInterval time in seconds a client needs to wait before sending polls to the
* server, a negative int indicates that there is no limit.
*/
public void setMaxPollingInterval(int maxPollingInterval) {
this.maxPollingInterval = maxPollingInterval;
}
/**
* Returns the max interval within which a client can send polling requests. If more than one
* request occurs in the interval the session will be terminated.
*
* @return the max interval within which a client can send polling requests. If more than one
* request occurs in the interval the session will be terminated.
*/
public int getMaxPollingInterval() {
return this.maxPollingInterval;
}
/**
* The max number of requests it is permissible for this session to have open at any one time.
*
* @param maxRequests The max number of requests it is permissible for this session to have open
* at any one time.
*/
public void setMaxRequests(int maxRequests) {
this.maxRequests = maxRequests;
}
/**
* Returns the max number of requests it is permissible for this session to have open at any one
* time.
*
* @return the max number of requests it is permissible for this session to have open at any one
* time.
*/
public int getMaxRequests() {
return this.maxRequests;
}
/**
* 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.
*
* @return true if all connections on this session should be secured, and false if they should
* not.
*/
@Override
public boolean isSecure() {
return isSecure;
}
/**
* 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 org.jivesoftware.openfire.http.SessionListener} to this session. The listener
* will be notified of changes to the session.
*
* @param listener the listener which is being added to the session.
*/
public void addSessionCloseListener(SessionListener listener) {
listeners.add(listener);
}
/**
* Removes a {@link org.jivesoftware.openfire.http.SessionListener} from this session. The
* listener will no longer be updated when an event occurs on the session.
*
* @param listener the session listener that is to be removed.
*/
public void removeSessionCloseListener(SessionListener listener) {
listeners.remove(listener);
}
/**
* Sets the 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.
*
* @param inactivityTimeout the time, in seconds, after which this session will be considered
* inactive and be terminated.
*/
public void setInactivityTimeout(int inactivityTimeout) {
this.inactivityTimeout = inactivityTimeout;
}
/**
* 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.
*
* @return the time, in seconds, after which this session will be considered inactive and
* terminated.
*/
public int getInactivityTimeout() {
return inactivityTimeout;
}
/**
* 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
synchronized (connectionQueue) {
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.
*
* @return the time in milliseconds since the epoch that this session was last active.
*/
public long getLastActivity() {
if (!connectionQueue.isEmpty()) {
synchronized (connectionQueue) {
for (HttpConnection connection : connectionQueue) {
// The session is currently active, set the last activity to the current time.
if (!(connection.isClosed())) {
lastActivity = System.currentTimeMillis();
break;
}
}
}
}
return lastActivity;
}
/**
* 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.
*
* @return the highest 'rid' attribute the server has received where it has also received
* all requests with lower 'rid' values.
*/
public long getLastAcknowledged() {
long ack = lastRequestID;
Collections.sort(connectionQueue, connectionComparator);
synchronized (connectionQueue) {
for (HttpConnection connection : connectionQueue) {
if (connection.getRequestId() == ack + 1) {
ack++;
}
}
}
return ack;
}
/**
* 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 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 major version of the BOSH XEP which the client is utilizing.
*/
public int getMajorVersion() {
if (this.majorVersion != -1) {
return this.majorVersion;
}
else {
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;
}
/**
* Sets whether the initial request on the session was secure.
*
* @param isSecure true if the initial request was secure and false if it wasn't.
*/
protected void setSecure(boolean isSecure) {
this.isSecure = isSecure;
}
/**
* Forwards a client request, which is related to a session, to the server. A connection is
* created and queued up in the provided session. When a connection reaches the top of a queue
* any pending packets bound for the client will be forwarded to the client through the
* connection.
*
* @param rid the unique, sequential, requestID sent from the client.
* @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
* was not.
* @param rootNode the XML body of the request.
* @param context the context of the asynchronous servlet call leading up to this method call.
*
* @throws org.jivesoftware.openfire.http.HttpBindException for several reasons: if the encoding inside of an auth packet is
* not recognized by the server, or if the packet type is not recognized.
* @throws org.jivesoftware.openfire.http.HttpConnectionClosedException if the session is no longer available.
*/
public void forwardRequest(long rid, boolean isSecure, Element rootNode, AsyncContext context)
throws HttpBindException, HttpConnectionClosedException, IOException
{
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 = this.createConnection(rid, isSecure, isPoll, context);
if (elements.size() > 0) {
// creates the runnable to forward the packets
packetsToSend.add(elements);
new HttpPacketSender(this).init();
}
final String type = rootNode.attributeValue("type");
String restartStream = rootNode.attributeValue(new QName("restart", rootNode.getNamespaceForPrefix("xmpp")));
int pauseDuration = HttpBindServlet.getIntAttribute(rootNode.attributeValue("pause"), -1);
if ("terminate".equals(type)) {
connection.deliverBody(createEmptyBody(true), true);
close();
lastRequestID = connection.getRequestId();
}
else if ("true".equals(restartStream) && rootNode.elements().size() == 0) {
connection.deliverBody(createSessionRestartResponse(), true);
lastRequestID = connection.getRequestId();
}
else if (pauseDuration > 0 && pauseDuration <= getMaxPause()) {
pause(pauseDuration);
connection.deliverBody(createEmptyBody(false), true);
lastRequestID = connection.getRequestId();
setLastResponseEmpty(true);
}
else {
resetInactivityTimeout();
}
}
/**
* This methods sends any pending packets in the session. If no packets are
* pending, this method simply returns. The method is internally synchronized
* to avoid simultaneous sending operations on this Session. If two
* threads try to run this method simultaneously, the first one will trigger
* the pending packets to be sent, while the second one will simply return
* (as there are no packets left to send).
*/
protected void sendPendingPackets() {
// access blocked only on send to prevent deadlocks
synchronized (packetsToSend) {
if (packetsToSend.isEmpty()) {
return;
}
if (router == null) {
router = new SessionPacketRouter(this);
}
for (Element packet : packetsToSend.remove()) {
try {
router.route(packet);
}
catch (UnknownStanzaException e) {
Log.error("Client provided unknown packet type", e);
}
}
}
}
/**
* Return the X509Certificates associated with this session.
*
* @return the X509Certificate associated with this session.
*/
@Override
public X509Certificate[] getPeerCertificates() {
return sslCertificates;
}
/**
* Creates a new connection on this session. If a response is currently available for this
* session the connection is responded to immediately, otherwise it is queued awaiting a
* response.
*
* @param rid the request id related to the connection.
* @param isSecure true if the connection was secured using HTTPS.
* @return the created {@link org.jivesoftware.openfire.http.HttpConnection} which represents
* the connection.
*
* @throws HttpConnectionClosedException if the connection was closed before a response could be
* delivered.
* @throws HttpBindException if the connection has violated a facet of the HTTP binding
* protocol.
*/
synchronized HttpConnection createConnection(long rid, boolean isSecure, boolean isPoll, AsyncContext context)
throws HttpConnectionClosedException, HttpBindException, IOException
{
final HttpConnection connection = new HttpConnection(rid, isSecure, sslCertificates, context);
connection.setSession(this);
context.setTimeout(getWait() * JiveConstants.SECOND);
context.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
Log.debug("complete event " + asyncEvent);
connectionQueue.remove(connection);
fireConnectionClosed(connection);
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
Log.debug("timeout event " + asyncEvent);
try {
// If onTimeout does not result in a complete(), the container falls back to default behavior.
// This is why this body is to be delivered in a non-async fashion.
connection.deliverBody(createEmptyBody(false), false);
setLastResponseEmpty(true);
// This connection timed out we need to increment the request count
if (connection.getRequestId() != lastRequestID + 1) {
throw new IOException("Unexpected RID error.");
}
lastRequestID = connection.getRequestId();
} catch (HttpConnectionClosedException e) {
Log.warn("Unexpected exception while processing connection timeout.", e);
}
// Note that 'onComplete' will be invoked.
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
Log.debug("error event " + asyncEvent);
Log.warn("Unhandled AsyncListener error: " + asyncEvent.getThrowable());
connectionQueue.remove(connection);
fireConnectionClosed(connection);
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {}
});
if (rid <= lastRequestID) {
Delivered deliverable = retrieveDeliverable(rid);
if (deliverable == null) {
Log.warn("Deliverable unavailable for " + rid);
throw new HttpBindException("Unexpected RID error.",
BoshBindingError.itemNotFound);
}
connection.deliverBody(createDeliverable(deliverable.deliverables), true);
addConnection(connection, isPoll);
return connection;
}
else if (rid > (lastRequestID + maxRequests)) {
Log.warn("Request " + rid + " > " + (lastRequestID + maxRequests) + ", ending session.");
throw new HttpBindException("Unexpected RID error.",
BoshBindingError.itemNotFound);
}
addConnection(connection, isPoll);
return connection;
}
private Delivered retrieveDeliverable(long rid) {
Delivered result = null;
synchronized (sentElements) {
for (Delivered delivered : sentElements) {
if (delivered.getRequestID() == rid) {
result = delivered;
break;
}
}
}
return result;
}
private void addConnection(HttpConnection connection, boolean isPoll) throws HttpBindException,
HttpConnectionClosedException, IOException {
if (connection == null) {
throw new IllegalArgumentException("Connection cannot be null.");
}
if (isSecure && !connection.isSecure()) {
throw new HttpBindException("Session was started from secure connection, all " +
"connections on this session must be secured.", BoshBindingError.badRequest);
}
final long rid = connection.getRequestId();
/*
* Search through the connection queue to see if this rid already exists on it. If it does then we
* will close and deliver the existing connection (if appropriate), and close and deliver the same
* deliverable on the new connection. This is under the assumption that a connection has been dropped,
* and re-requested before jetty has realised.
*/
synchronized (connectionQueue) {
for (HttpConnection queuedConnection : connectionQueue) {
if (queuedConnection.getRequestId() == rid) {
if(Log.isDebugEnabled()) {
Log.debug("Found previous connection in queue with rid " + rid);
}
if(queuedConnection.isClosed()) {
if(Log.isDebugEnabled()) {
Log.debug("It's closed - copying deliverables");
}
Delivered deliverable = retrieveDeliverable(rid);
if (deliverable == null) {
Log.warn("Deliverable unavailable for " + rid);
throw new HttpBindException("Unexpected RID error.",
BoshBindingError.itemNotFound);
}
connection.deliverBody(createDeliverable(deliverable.deliverables), true);
} else {
if(Log.isDebugEnabled()) {
Log.debug("It's still open - calling close()");
}
deliver(queuedConnection, Collections.singleton(new Deliverable("")));
connection.close();
if(rid == (lastRequestID + 1)) {
lastRequestID = rid;
}
}
break;
}
}
}
checkOveractivity(isPoll);
sslCertificates = connection.getPeerCertificates();
// We aren't supposed to hold connections open or we already have some packets waiting
// to be sent to the client.
if (isPollingSession() || (pendingElements.size() > 0 && connection.getRequestId() == lastRequestID + 1)) {
fireConnectionOpened(connection);
synchronized(pendingElements) {
deliver(connection, pendingElements);
lastRequestID = connection.getRequestId();
pendingElements.clear();
}
}
else {
// With this connection we need to check if we will have too many connections open,
// closing any extras.
connectionQueue.add(connection);
Collections.sort(connectionQueue, connectionComparator);
synchronized (connectionQueue) {
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() && toClose.getRequestId() == lastRequestID + 1) {
if(toClose == connection) {
// Current connection has no continuation yet, just deliver.
deliver("");
}
else {
toClose.close();
}
lastRequestID = toClose.getRequestId();
closed++;
}
}
}
}
}
private int getOpenConnectionCount() {
int count = 0;
// NOTE: synchronized by caller
for (HttpConnection connection : connectionQueue) {
if (!connection.isClosed()) {
count++;
}
}
return count;
}
private void deliver(HttpConnection connection, Collection deliverable)
throws HttpConnectionClosedException, IOException {
connection.deliverBody(createDeliverable(deliverable), true);
Delivered delivered = new Delivered(deliverable);
delivered.setRequestID(connection.getRequestId());
while (sentElements.size() > maxRequests) {
sentElements.remove(0);
}
sentElements.add(delivered);
}
private void fireConnectionOpened(HttpConnection connection) {
lastActivity = System.currentTimeMillis();
for (SessionListener listener : listeners) {
listener.connectionOpened(this, connection);
}
}
/**
* 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";
synchronized (connectionQueue) {
for (HttpConnection conn : connectionQueue) {
if (!conn.isClosed()) {
pendingConnections++;
}
}
}
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);
}
}
}
private void deliver(String text) {
if (text == null) {
// Do nothing if someone asked to send nothing :)
return;
}
deliver(new Deliverable(text));
}
@Override
public void deliver(Packet stanza) {
deliver(new Deliverable(Arrays.asList(stanza)));
}
private void deliver(Deliverable stanza) {
Collection deliverable = Arrays.asList(stanza);
boolean delivered = false;
int pendingConnections = 0;
synchronized (connectionQueue) {
for (HttpConnection connection : connectionQueue) {
if (connection.isClosed()) {
continue;
}
pendingConnections++;
try {
if (connection.getRequestId() == lastRequestID + 1) {
lastRequestID = connection.getRequestId();
deliver(connection, deliverable);
delivered = true;
break;
}
}
catch (HttpConnectionClosedException e) {
/* Connection was closed, try the next one. Indicates a (concurrency?) bug. */
Log.warn("Iterating over a connection that was closed. Openfire will recover from this problem, but it should not occur in the first place.");
} catch (IOException e) {
Log.warn("An unexpected exception occurred while iterating over connections. Openfire will attempt to recover by ignoring this connection.", e);
}
}
}
if (!delivered) {
if (pendingConnections > 0) {
Log.warn("Unable to deliver a stanza (it is being queued instead), although there are available connections! RID / Connection processing is out of sync!");
}
pendingElements.add(stanza);
}
}
private void fireConnectionClosed(HttpConnection connection) {
lastActivity = System.currentTimeMillis();
for (SessionListener listener : listeners) {
listener.connectionClosed(this, connection);
}
}
private String createDeliverable(Collection elements) {
StringBuilder builder = new StringBuilder();
builder.append("");
setLastResponseEmpty(elements.size() == 0);
synchronized (elements) {
for (Deliverable child : elements) {
builder.append(child.getDeliverable());
}
}
builder.append("");
return builder.toString();
}
private void closeSession() {
if (isClosed) { return; }
isClosed = true;
try {
// close connection(s) and deliver pending elements (if any)
synchronized (connectionQueue) {
for (HttpConnection toClose : connectionQueue) {
try {
if (!toClose.isClosed()) {
if (!pendingElements.isEmpty() && toClose.getRequestId() == lastRequestID + 1) {
synchronized(pendingElements) {
deliver(toClose, pendingElements);
lastRequestID = toClose.getRequestId();
pendingElements.clear();
}
} else {
toClose.deliverBody(null, true);
}
}
} catch (HttpConnectionClosedException e) {
/* ignore ... already closed */
} catch (IOException e) {
// Likely caused by closing a stale session / connection.
Log.debug("An unexpected exception occurred while closing a session.", e);
}
}
}
synchronized (pendingElements) {
for (Deliverable deliverable : pendingElements) {
failDelivery(deliverable.getPackets());
}
pendingElements.clear();
}
} finally { // ensure the session is removed from the session map
for (SessionListener listener : listeners) {
listener.sessionClosed(this);
}
this.listeners.clear();
}
}
private void failDelivery(final Collection packets) {
if (packets == null) {
// Do nothing if someone asked to deliver nothing :)
return;
}
// use a separate thread to schedule backup delivery
TaskEngine.getInstance().submit(new Runnable() {
@Override
public void run() {
for (Packet packet : packets) {
try {
backupDeliverer.deliver(packet);
}
catch (UnauthorizedException e) {
Log.error("Unable to deliver message to backup deliverer", e);
}
}
}
});
}
protected String createEmptyBody(boolean terminate)
{
final Element body = DocumentHelper.createElement( QName.get( "body", "http://jabber.org/protocol/httpbind" ) );
if (terminate) { body.addAttribute("type", "terminate"); }
body.addAttribute("ack", String.valueOf(getLastAcknowledged()));
return body.asXML();
}
private String createSessionRestartResponse()
{
final Element response = DocumentHelper.createElement( QName.get( "body", "http://jabber.org/protocol/httpbind" ) );
response.addNamespace("stream", "http://etherx.jabber.org/streams");
final Element features = response.addElement("stream:features");
for (Element feature : getAvailableStreamFeaturesElements()) {
features.add(feature);
}
return response.asXML();
}
/**
* A virtual server connection relates to a http session which its self can relate to many http
* connections.
*/
public static class HttpVirtualConnection extends VirtualConnection {
private InetAddress address;
private ConnectionConfiguration configuration;
private ConnectionType connectionType;
public HttpVirtualConnection(InetAddress address) {
this.address = address;
this.connectionType = ConnectionType.SOCKET_C2S;
}
public HttpVirtualConnection(InetAddress address, ConnectionType connectionType) {
this.address = address;
this.connectionType = connectionType;
}
@Override
public void closeVirtualConnection() {
((HttpSession) session).closeSession();
}
@Override
public byte[] getAddress() throws UnknownHostException {
return address.getAddress();
}
@Override
public String getHostAddress() throws UnknownHostException {
return address.getHostAddress();
}
@Override
public String getHostName() throws UnknownHostException {
return address.getHostName();
}
@Override
public void systemShutdown() {
close();
}
@Override
public void deliver(Packet packet) throws UnauthorizedException {
((HttpSession) session).deliver(packet);
}
@Override
public void deliverRawText(String text) {
((HttpSession) session).deliver(text);
}
@Override
public ConnectionConfiguration getConfiguration() {
if (configuration == null) {
final ConnectionManagerImpl connectionManager = ((ConnectionManagerImpl) XMPPServer.getInstance().getConnectionManager());
configuration = connectionManager.getListener( connectionType, true ).generateConnectionConfiguration();
}
return configuration;
}
@Override
public Certificate[] getPeerCertificates() {
return ((HttpSession) session).getPeerCertificates();
}
}
static class Deliverable {
private final String text;
private final Collection packets;
public Deliverable(String text) {
this.text = text;
this.packets = null;
}
public Deliverable(Collection elements) {
this.text = null;
this.packets = new ArrayList<>();
for (Packet packet : elements) {
// Append packet namespace according XEP-0206 if needed
if (Namespace.NO_NAMESPACE.equals(packet.getElement().getNamespace())) {
// use string-based operation here to avoid cascading xmlns wonkery
StringBuilder packetXml = new StringBuilder(packet.toXML());
final int noslash = packetXml.indexOf( ">" );
final int slash = packetXml.indexOf( "/>" );
final int insertAt = ( noslash - 1 == slash ? slash : noslash );
packetXml.insert( insertAt, " xmlns=\"jabber:client\"");
this.packets.add(packetXml.toString());
} else {
this.packets.add(packet.toXML());
}
}
}
public String getDeliverable() {
if (text == null) {
StringBuilder builder = new StringBuilder();
for (String packet : packets) {
builder.append(packet);
}
return builder.toString();
}
else {
return text;
}
}
public Collection getPackets() {
// Check if the Deliverable is about Packets or raw XML
if (packets == null) {
// No packets here (should be just raw XML like so return nothing
return null;
}
List answer = new ArrayList<>();
for (String packetXML : packets) {
try {
Packet packet = null;
// Parse the XML stanza
Element element = localParser.get().read(new StringReader(packetXML)).getRootElement();
String tag = element.getName();
if ("message".equals(tag)) {
packet = new Message(element, true);
}
else if ("presence".equals(tag)) {
packet = new Presence(element, true);
}
else if ("iq".equals(tag)) {
packet = new IQ(element, true);
}
// Add the reconstructed packet to the result
answer.add(packet);
}
catch (Exception e) {
Log.error("Error while parsing Privacy Property", e);
}
}
return answer;
}
}
private class Delivered {
private long requestID;
private Collection deliverables;
public Delivered(Collection deliverables) {
this.deliverables = deliverables;
}
public void setRequestID(long requestID) {
this.requestID = requestID;
}
public long getRequestID() {
return requestID;
}
public Collection getPackets() {
List packets = new ArrayList<>();
synchronized (deliverables) {
for (Deliverable deliverable : deliverables) {
if (deliverable.packets != null) {
packets.addAll(deliverable.getPackets());
}
}
}
return packets;
}
}
/**
* A runner that guarantees that the packets per a session will be sent and
* processed in the order in which they were received.
*/
private class HttpPacketSender implements Runnable {
private HttpSession session;
HttpPacketSender(HttpSession session) {
this.session = session;
}
@Override
public void run() {
session.sendPendingPackets();
}
private void init() {
HttpBindManager.getInstance().getSessionManager().execute(this);
}
}
}