/**
* $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;
import org.dom4j.Element;
import org.jivesoftware.multiplexer.spi.ClientFailoverDeliverer;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* Session that represents a client to server connection.
*
* @author Gaston Dombiak
*/
public class ClientSession extends Session {
private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";
private static ConnectionCloseListener closeListener;
static {
closeListener = new ConnectionCloseListener() {
public void onConnectionClose(Object handback) {
ClientSession session = (ClientSession) handback;
// Mark the session as closed
session.close(false);
}
};
}
public static Session createSession(String serverName, XmlPullParser xpp, Connection connection)
throws XmlPullParserException {
boolean isFlashClient = xpp.getPrefix().equals("flash");
connection.setFlashClient(isFlashClient);
// Conduct error checking, the opening tag should be 'stream'
// in the 'etherx' namespace
if (!xpp.getName().equals("stream") && !isFlashClient) {
throw new XmlPullParserException(
LocaleUtils.getLocalizedString("admin.error.bad-stream"));
}
if (!xpp.getNamespace(xpp.getPrefix()).equals(ETHERX_NAMESPACE) &&
!(isFlashClient && xpp.getNamespace(xpp.getPrefix()).equals(FLASH_NAMESPACE)))
{
throw new XmlPullParserException(LocaleUtils.getLocalizedString(
"admin.error.bad-namespace"));
}
// TODO Check if IP address is allowed to connect to the server
// Default language is English ("en").
String language = "en";
// Default to a version of "0.0". Clients written before the XMPP 1.0 spec may
// not report a version in which case "0.0" should be assumed (per rfc3920
// section 4.4.1).
int majorVersion = 0;
int minorVersion = 0;
for (int i = 0; i < xpp.getAttributeCount(); i++) {
if ("lang".equals(xpp.getAttributeName(i))) {
language = xpp.getAttributeValue(i);
}
if ("version".equals(xpp.getAttributeName(i))) {
try {
int[] version = decodeVersion(xpp.getAttributeValue(i));
majorVersion = version[0];
minorVersion = version[1];
}
catch (Exception e) {
Log.error(e);
}
}
}
// If the client supports a greater major version than the server,
// set the version to the highest one the server supports.
if (majorVersion > MAJOR_VERSION) {
majorVersion = MAJOR_VERSION;
minorVersion = MINOR_VERSION;
}
else if (majorVersion == MAJOR_VERSION) {
// If the client supports a greater minor version than the
// server, set the version to the highest one that the server
// supports.
if (minorVersion > MINOR_VERSION) {
minorVersion = MINOR_VERSION;
}
}
// Store language and version information in the connection.
connection.setLanaguage(language);
connection.setXMPPVersion(majorVersion, minorVersion);
ServerSurrogate serverSurrogate = ConnectionManager.getInstance().getServerSurrogate();
// Indicate the TLS policy to use for this connection
connection.setTlsPolicy(serverSurrogate.getTlsPolicy());
// Indicate the compression policy to use for this connection
connection.setCompressionPolicy(serverSurrogate.getCompressionPolicy());
// Create a ClientSession for this user.
String streamID = idFactory.createStreamID();
ClientSession session = new ClientSession(serverName, connection, streamID);
connection.init(session);
// Set the stream ID that identifies the client when forwarding traffic to a client fails
((ClientFailoverDeliverer) connection.getPacketDeliverer()).setStreamID(streamID);
// Listen when the connection is closed
connection.registerCloseListener(closeListener, session);
// Register that the new session is associated with the specified stream ID
Session.addSession(streamID, session);
// Send to the server that a new client session has been created
InetAddress address = null;
try {
address = connection.getInetAddress();
} catch (UnknownHostException e) {
// Do nothing
}
serverSurrogate.clientSessionCreated(streamID, address);
// Build the start packet response
StringBuilder sb = new StringBuilder(200);
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
if (isFlashClient) {
sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
}
else {
sb.append("<stream:stream ");
}
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
sb.append(serverName);
sb.append("\" id=\"");
sb.append(session.getStreamID());
sb.append("\" xml:lang=\"");
sb.append(language);
// Don't include version info if the version is 0.0.
if (majorVersion != 0) {
sb.append("\" version=\"");
sb.append(majorVersion).append(".").append(minorVersion);
}
sb.append("\">");
connection.deliverRawText(sb.toString());
// If this is a "Jabber" connection, the session is now initialized and we can
// return to allow normal packet parsing.
if (majorVersion == 0) {
return session;
}
// Otherwise, this is at least XMPP 1.0 so we need to announce stream features.
sb = new StringBuilder(490);
sb.append("<stream:features>");
if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) {
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (connection.getTlsPolicy() == Connection.TLSPolicy.required) {
sb.append("<required/>");
}
sb.append("</starttls>");
}
// Include available SASL Mechanisms
sb.append(serverSurrogate.getSASLMechanisms(session));
// Include Stream features
String specificFeatures = session.getAvailableStreamFeatures();
if (specificFeatures != null) {
sb.append(specificFeatures);
}
sb.append("</stream:features>");
connection.deliverRawText(sb.toString());
return session;
}
public ClientSession(String serverName, Connection connection, String streamID) {
super(serverName, connection, streamID);
}
public String getAvailableStreamFeatures() {
// Offer authenticate and registration only if TLS was not required or if required
// then the connection is already secured
if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) {
return null;
}
StringBuilder sb = new StringBuilder(200);
// TODO Fix compression with MINA and re-enable this code
// Include Stream Compression Mechanism
/*if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
sb.append(
"<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
}*/
if (getStatus() != Session.STATUS_AUTHENTICATED) {
ServerSurrogate serverSurrogate = ConnectionManager.getInstance().getServerSurrogate();
// Advertise that the server supports Non-SASL Authentication
if (serverSurrogate.isNonSASLAuthEnabled()) {
sb.append("<auth xmlns=\"http://jabber.org/features/iq-auth\"/>");
}
// Advertise that the server supports In-Band Registration
if (serverSurrogate.isInbandRegEnabled()) {
sb.append("<register xmlns=\"http://jabber.org/features/iq-register\"/>");
}
}
else {
// If the session has been authenticated then offer resource binding
// and session establishment
sb.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"/>");
sb.append("<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>");
}
return sb.toString();
}
/**
* Delivers a stanza sent by the server to the client.
*
* @param stanza the stanza sent by the server.
*/
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 stanza to client
if (conn != null && !conn.isClosed()) {
try {
conn.deliver(stanza.asXML());
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
}
}
public void close() {
close(false);
}
/**
* Closes the client connection. The <tt>systemStopped</tt> parameter indicates if the
* client connection is being closed because the server is shutting down or unavailable
* or if it is because the connection manager is being shutdown.
*
* @param systemStopped true when the server is no longer available or the
* connection manager is being shutdown.
*/
public void close(boolean systemStopped) {
if (status != STATUS_CLOSED) {
// Change the status to closed
status = STATUS_CLOSED;
// Close the connection of the client
if (systemStopped) {
conn.systemShutdown();
}
else {
conn.close();
}
// Remove session from list of sessions
removeSession(getStreamID());
// Tell the server that the client session has been closed
ConnectionManager.getInstance().getServerSurrogate().clientSessionClosed(getStreamID());
}
}
public boolean isClosed() {
return status == STATUS_CLOSED;
}
}