/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2006 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.multiplexer.net.http;
import org.dom4j.*;
import org.jivesoftware.multiplexer.ConnectionManager;
import org.jivesoftware.multiplexer.ServerSurrogate;
import org.jivesoftware.multiplexer.Session;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import java.util.*;
/**
*
*/
public class HttpSessionManager {
/**
* Milliseconds a connection has to be idle to be closed. Default is 30 minutes. Sending
* stanzas to the client is not considered as activity. We are only considering the connection
* active when the client sends some data or hearbeats (i.e. whitespaces) to the server.
* The reason for this is that sending data will fail if the connection is closed. And if
* the thread is blocked while sending data (because the socket is closed) then the clean up
* thread will close the socket anyway.
*/
private static int inactivityTimeout;
/**
* The connection manager MAY limit the number of simultaneous requests the client makes with
* the 'requests' attribute. The RECOMMENDED value is "2". Servers that only support polling
* behavior MUST prevent clients from making simultaneous requests by setting the 'requests'
* attribute to a value of "1" (however, polling is NOT RECOMMENDED). In any case, clients MUST
* NOT make more simultaneous requests than specified by the connection manager.
*/
private static int maxRequests;
/**
* The connection manager SHOULD include two additional attributes in the session creation
* response element, specifying the shortest allowable polling interval and the longest
* allowable inactivity period (both in seconds). Communication of these parameters enables
* the client to engage in appropriate behavior (e.g., not sending empty request elements more
* often than desired, and ensuring that the periods with no requests pending are
* never too long).
*/
private static int pollingInterval;
private String serverName;
private ServerSurrogate serverSurrogate;
private InactivityTimer timer = new InactivityTimer();
static {
// Set the default read idle timeout. If none was set then assume 30 minutes
inactivityTimeout = JiveGlobals.getIntProperty("xmpp.httpbind.client.idle", 30);
maxRequests = JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.max", 2);
pollingInterval = JiveGlobals.getIntProperty("xmpp.httpbind.client.requests.polling", 5);
}
public HttpSessionManager(String serverName) {
this.serverName = serverName;
this.serverSurrogate = ConnectionManager.getInstance().getServerSurrogate();
}
public HttpSession getSession(String streamID) {
Session session = Session.getSession(streamID);
if(session instanceof HttpSession) {
return (HttpSession) session;
}
return null;
}
public HttpSession createSession(Element rootNode, HttpConnection connection)
throws HttpBindException
{
// TODO Check if IP address is allowed to connect to the server
// Default language is English ("en").
String language = rootNode.attributeValue("xml:lang");
if(language == null || "".equals(language)) {
language = "en";
}
int wait = getIntAttribute(rootNode.attributeValue("wait"), 60);
int hold = getIntAttribute(rootNode.attributeValue("hold"), 1);
// Indicate the compression policy to use for this connection
connection.setCompressionPolicy(serverSurrogate.getCompressionPolicy());
HttpSession session = createSession(serverName);
session.setWait(wait);
session.setHold(hold);
session.setSecure(connection.isSecure());
session.setMaxPollingInterval(pollingInterval);
session.setInactivityTimeout(inactivityTimeout);
// Store language and version information in the connection.
session.setLanaguage(language);
try {
connection.deliverBody(createSessionCreationResponse(session));
}
catch (HttpConnectionClosedException e) {
/* This won't happen here. */
}
catch (DocumentException e) {
Log.error("Error creating document", e);
throw new HttpBindException("Internal server error", true, 500);
}
timer.reset(session);
return session;
}
private HttpSession createSession(String serverName) {
// Create a ClientSession for this user.
String streamID = Session.idFactory.createStreamID();
HttpSession session = new HttpSession(serverName, streamID);
// Register that the new session is associated with the specified stream ID
HttpSession.addSession(streamID, session);
// Send to the server that a new client session has been created
serverSurrogate.clientSessionCreated(streamID);
session.addSessionCloseListener(new SessionListener() {
public void connectionOpened(Session session, HttpConnection connection) {
if (session instanceof HttpSession) {
timer.stop((HttpSession) session);
}
}
public void connectionClosed(Session session, HttpConnection connection) {
if(session instanceof HttpSession) {
HttpSession http = (HttpSession) session;
if(http.getConnectionCount() <= 0) {
timer.reset(http);
}
}
}
public void sessionClosed(Session session) {
HttpSession.removeSession(session.getStreamID());
if(session instanceof HttpSession) {
timer.stop((HttpSession) session);
}
serverSurrogate.clientSessionClosed(session.getStreamID());
}
});
return session;
}
private static int getIntAttribute(String value, int defaultValue) {
if(value == null || "".equals(value)) {
return defaultValue;
}
try {
return Integer.valueOf(value);
}
catch (Exception ex) {
return defaultValue;
}
}
private String createSessionCreationResponse(HttpSession session) throws DocumentException {
Element response = DocumentHelper.createElement("body");
response.addNamespace("", "http://jabber.org/protocol/httpbind");
response.addNamespace("stream", "http://etherx.jabber.org/streams");
response.addAttribute("authid", session.getStreamID());
response.addAttribute("sid", session.getStreamID());
response.addAttribute("secure", Boolean.TRUE.toString());
response.addAttribute("requests", String.valueOf(maxRequests));
response.addAttribute("inactivity", String.valueOf(session.getInactivityTimeout()));
response.addAttribute("polling", String.valueOf(pollingInterval));
response.addAttribute("wait", String.valueOf(session.getWait()));
Element features = response.addElement("stream:features");
features.add(serverSurrogate.getSASLMechanismsElement(session).createCopy());
Element bind = DocumentHelper.createElement(new QName("bind",
new Namespace("", "urn:ietf:params:xml:ns:xmpp-bind")));
features.add(bind);
Element sessionElement = DocumentHelper.createElement(new QName("session",
new Namespace("", "urn:ietf:params:xml:ns:xmpp-session")));
features.add(sessionElement);
return response.asXML();
}
public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure,
Element rootNode) throws HttpBindException,
HttpConnectionClosedException
{
//noinspection unchecked
List<Element> elements = rootNode.elements();
boolean isPoll = elements.size() <= 0;
HttpConnection connection = new HttpConnection(rid, isSecure);
session.addConnection(connection, isPoll);
for (Element packet : elements) {
serverSurrogate.send(packet.asXML(), session.getStreamID());
}
return connection;
}
private class InactivityTimer extends Timer {
private Map<String, InactivityTimeoutTask> sessionMap
= new HashMap<String, InactivityTimeoutTask>();
public void stop(HttpSession session) {
InactivityTimeoutTask task = sessionMap.remove(session.getStreamID());
if(task != null) {
task.cancel();
}
}
public void reset(HttpSession session) {
stop(session);
if(session.isClosed()) {
return;
}
InactivityTimeoutTask task = new InactivityTimeoutTask(session);
schedule(task, session.getInactivityTimeout() * 1000);
sessionMap.put(session.getStreamID(), task);
}
}
private class InactivityTimeoutTask extends TimerTask {
private Session session;
public InactivityTimeoutTask(Session session) {
this.session = session;
}
public void run() {
session.close();
timer.sessionMap.remove(session.getStreamID());
}
}
}