Commit 109c633c authored by Gaston Dombiak's avatar Gaston Dombiak Committed by gato

Initial check in.

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@8402 b35dd754-fafc-0310-a699-88a17e54d16e
parent 3b3318fa
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 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.openfire;
import org.jivesoftware.openfire.session.*;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.TaskEngine;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* A LocalSessionManager keeps track of sessions that are connected to this JVM and for
* which there is no route. That is, sessions that are added to the routing table are
* not going to be stored by this manager.<p>
*
* For external component sessions, incoming server sessions and connection manager
* sessions there is never going to be a route so they are only kept here. Client
* sessions before they authenticate are kept in this manager but once authenticated
* they are removed since a new route is created for authenticated client sessions.<p>
*
* Sessions stored in this manager are not accessible from other cluster nodes. However,
* sessions for which there is a route in the routing table can be accessed from other
* cluster nodes. The only exception to this rule are the sessions of external components.
* External component sessions are kept in this manager but all components (internal and
* external) create a route in the routing table for the service they provide. That means
* that services of components are accessible from other cluster nodes and when the
* component is an external component then its session will be used to deliver packets
* through the external component's session.
*
* @author Gaston Dombiak
*/
class LocalSessionManager {
/**
* Map that holds sessions that has been created but haven't been authenticated yet. The Map
* will hold client sessions.
*/
private Map<String, LocalClientSession> preAuthenticatedSessions = new ConcurrentHashMap<String, LocalClientSession>();
/**
* The sessions contained in this List are component sessions. For each connected component
* this Map will keep the component's session.
*/
private List<LocalComponentSession> componentsSessions = new CopyOnWriteArrayList<LocalComponentSession>();
/**
* Map of connection multiplexer sessions grouped by connection managers. Each connection
* manager may have many connections to the server (i.e. connection pool). All connections
* originated from the same connection manager are grouped as a single entry in the map.
* Once all connections have been closed users that were logged using the connection manager
* will become unavailable.
*/
private Map<String, LocalConnectionMultiplexerSession> connnectionManagerSessions =
new ConcurrentHashMap<String, LocalConnectionMultiplexerSession>();
/**
* The sessions contained in this Map are server sessions originated by a remote server. These
* sessions can only receive packets from the remote server but are not capable of sending
* packets to the remote server. Sessions will be added to this collecion only after they were
* authenticated.
* Key: streamID, Value: the IncomingServerSession associated to the streamID.
*/
private final Map<String, LocalIncomingServerSession> incomingServerSessions =
new ConcurrentHashMap<String, LocalIncomingServerSession>();
public Map<String, LocalClientSession> getPreAuthenticatedSessions() {
return preAuthenticatedSessions;
}
public List<LocalComponentSession> getComponentsSessions() {
return componentsSessions;
}
public Map<String, LocalConnectionMultiplexerSession> getConnnectionManagerSessions() {
return connnectionManagerSessions;
}
public LocalIncomingServerSession getIncomingServerSessions(String streamID) {
return incomingServerSessions.get(streamID);
}
public void addIncomingServerSessions(String streamID, LocalIncomingServerSession session) {
incomingServerSessions.put(streamID, session);
}
public void removeIncomingServerSessions(String streamID) {
incomingServerSessions.remove(streamID);
}
public void start() {
// Run through the server sessions every 3 minutes after a 3 minutes server startup delay (default values)
int period = 3 * 60 * 1000;
TaskEngine.getInstance().scheduleAtFixedRate(new ServerCleanupTask(), period, period);
}
public void stop() {
try {
// Send the close stream header to all connected connections
Set<LocalSession> sessions = new HashSet<LocalSession>();
sessions.addAll(preAuthenticatedSessions.values());
sessions.addAll(componentsSessions);
for (LocalIncomingServerSession incomingSession : incomingServerSessions.values()) {
sessions.add(incomingSession);
}
for (LocalConnectionMultiplexerSession multiplexer : connnectionManagerSessions.values()) {
sessions.add(multiplexer);
}
for (LocalSession session : sessions) {
try {
// Notify connected client that the server is being shut down
session.getConnection().systemShutdown();
}
catch (Throwable t) {
// Ignore.
}
}
}
catch (Exception e) {
// Ignore.
}
}
/**
* Task that closes idle server sessions.
*/
private class ServerCleanupTask extends TimerTask {
/**
* Close incoming server sessions that have been idle for a long time.
*/
public void run() {
// Do nothing if this feature is disabled
int idleTime = SessionManager.getInstance().getServerSessionIdleTime();
if (idleTime == -1) {
return;
}
final long deadline = System.currentTimeMillis() - idleTime;
for (LocalIncomingServerSession session : incomingServerSessions.values()) {
try {
if (session.getLastActiveDate().getTime() < deadline) {
session.close();
}
}
catch (Throwable e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
}
}
}
}
/**
* $RCSfile$
* $Revision: 3187 $
* $Date: 2005-12-11 13:34:34 -0300 (Sun, 11 Dec 2005) $
*
* Copyright (C) 2007 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.openfire.session;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.user.PresenceEventDispatcher;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import org.xmpp.packet.StreamError;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
/**
* Represents a session between the server and a client.
*
* @author Gaston Dombiak
*/
public class LocalClientSession extends LocalSession implements ClientSession {
private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";
/**
* Keep the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server.<p>
*
* Note: Key = IP address or IP range; Value = empty string. A hash map is being used for
* performance reasons.
*/
private static Map<String,String> allowedIPs = new HashMap<String,String>();
private static Connection.TLSPolicy tlsPolicy;
private static Connection.CompressionPolicy compressionPolicy;
/**
* The authentication token for this session.
*/
protected AuthToken authToken;
/**
* Flag indicating if this session has been initialized yet (upon first available transition).
*/
private boolean initialized;
/**
* Flag that indicates if the session was available ever.
*/
private boolean wasAvailable = false;
/**
* Flag indicating if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*/
private boolean offlineFloodStopped = false;
private Presence presence = null;
private int conflictCount = 0;
/**
* Privacy list that overrides the default privacy list. This list affects only this
* session and only for the duration of the session.
*/
private String activeList;
/**
* Default privacy list used for the session's user. This list is processed if there
* is no active list set for the session.
*/
private String defaultList;
static {
// Fill out the allowedIPs with the system property
String allowed = JiveGlobals.getProperty("xmpp.client.login.allowed", "");
StringTokenizer tokens = new StringTokenizer(allowed, ", ");
while (tokens.hasMoreTokens()) {
String address = tokens.nextToken().trim();
allowedIPs.put(address, "");
}
// Set the TLS policy stored as a system property
String policyName = JiveGlobals.getProperty("xmpp.client.tls.policy",
Connection.TLSPolicy.optional.toString());
tlsPolicy = Connection.TLSPolicy.valueOf(policyName);
// Set the Compression policy stored as a system property
policyName = JiveGlobals.getProperty("xmpp.client.compression.policy",
Connection.CompressionPolicy.optional.toString());
compressionPolicy = Connection.CompressionPolicy.valueOf(policyName);
}
/**
* Returns the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server.
*
* @return the list of IP address that are allowed to connect to the server.
*/
public static Map<String, String> getAllowedIPs() {
return allowedIPs;
}
/**
* Returns a newly created session between the server and a client. The session will
* be created and returned only if correct name/prefix (i.e. 'stream' or 'flash')
* and namespace were provided by the client.
*
* @param serverName the name of the server where the session is connecting to.
* @param xpp the parser that is reading the provided XML through the connection.
* @param connection the connection with the client.
* @return a newly created session between the server and a client.
* @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing incoming data.
*/
public static LocalClientSession 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"));
}
if (!allowedIPs.isEmpty()) {
boolean forbidAccess = false;
String hostAddress = "Unknown";
// The server is using a whitelist so check that the IP address of the client
// is authorized to connect to the server
try {
hostAddress = connection.getHostAddress();
if (!allowedIPs.containsKey(hostAddress)) {
byte[] address = connection.getAddress();
String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." +
(address[2] & 0xff) +
".*";
String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
String range3 = (address[0] & 0xff) + ".*.*.*";
if (!allowedIPs.containsKey(range1) && !allowedIPs.containsKey(range2) &&
!allowedIPs.containsKey(range3)) {
forbidAccess = true;
}
}
} catch (UnknownHostException e) {
forbidAccess = true;
}
if (forbidAccess) {
// Client cannot connect from this IP address so end the stream and
// TCP connection
Log.debug("Closed connection to client attempting to connect from: " + hostAddress);
// Include the not-authorized error in the response
StreamError error = new StreamError(StreamError.Condition.not_authorized);
connection.deliverRawText(error.toXML());
// Close the underlying connection
connection.close();
return null;
}
}
// 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);
// Indicate the TLS policy to use for this connection
if (!connection.isSecure()) {
boolean hasCertificates = false;
try {
hasCertificates = SSLConfig.getKeyStore().size() > 0;
}
catch (Exception e) {
Log.error(e);
}
if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
Log.error("Client session rejected. TLS is required but no certificates " +
"were created.");
return null;
}
// Set default TLS policy
connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
} else {
// Set default TLS policy
connection.setTlsPolicy(Connection.TLSPolicy.disabled);
}
// Indicate the compression policy to use for this connection
connection.setCompressionPolicy(compressionPolicy);
// Create a ClientSession for this user.
LocalClientSession session = SessionManager.getInstance().createClientSession(connection);
// 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().toString());
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(SASLAuthentication.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;
}
/**
* Sets the list of IP address that are allowed to connect to the server. If the list is
* empty then anyone is allowed to connect to the server.
*
* @param allowed the list of IP address that are allowed to connect to the server.
*/
public static void setAllowedIPs(Map<String, String> allowed) {
allowedIPs = allowed;
if (allowedIPs.isEmpty()) {
JiveGlobals.deleteProperty("xmpp.client.login.allowed");
}
else {
// Iterate through the elements in the map.
StringBuilder buf = new StringBuilder();
Iterator<String> iter = allowedIPs.keySet().iterator();
if (iter.hasNext()) {
buf.append(iter.next());
}
while (iter.hasNext()) {
buf.append(", ").append(iter.next());
}
JiveGlobals.setProperty("xmpp.client.login.allowed", buf.toString());
}
}
/**
* Returns whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @return whether TLS is mandatory, optional or is disabled.
*/
public static SocketConnection.TLSPolicy getTLSPolicy() {
return tlsPolicy;
}
/**
* Sets whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @param policy whether TLS is mandatory, optional or is disabled.
*/
public static void setTLSPolicy(SocketConnection.TLSPolicy policy) {
tlsPolicy = policy;
JiveGlobals.setProperty("xmpp.client.tls.policy", tlsPolicy.toString());
}
/**
* Returns whether compression is optional or is disabled for clients.
*
* @return whether compression is optional or is disabled.
*/
public static SocketConnection.CompressionPolicy getCompressionPolicy() {
return compressionPolicy;
}
/**
* Sets whether compression is optional or is disabled for clients.
*
* @param policy whether compression is optional or is disabled.
*/
public static void setCompressionPolicy(SocketConnection.CompressionPolicy policy) {
compressionPolicy = policy;
JiveGlobals.setProperty("xmpp.client.compression.policy", compressionPolicy.toString());
}
/**
* Returns the Privacy list that overrides the default privacy list. This list affects
* only this session and only for the duration of the session.
*
* @return the Privacy list that overrides the default privacy list.
*/
public PrivacyList getActiveList() {
if (activeList != null) {
try {
return PrivacyListManager.getInstance().getPrivacyList(getUsername(), activeList);
} catch (UserNotFoundException e) {
Log.error(e);
}
}
return null;
}
/**
* Sets the Privacy list that overrides the default privacy list. This list affects
* only this session and only for the duration of the session.
*
* @param activeList the Privacy list that overrides the default privacy list.
*/
public void setActiveList(PrivacyList activeList) {
this.activeList = activeList != null ? activeList.getName() : null;
}
/**
* Returns the default Privacy list used for the session's user. This list is
* processed if there is no active list set for the session.
*
* @return the default Privacy list used for the session's user.
*/
public PrivacyList getDefaultList() {
if (defaultList != null) {
try {
return PrivacyListManager.getInstance().getPrivacyList(getUsername(), defaultList);
} catch (UserNotFoundException e) {
Log.error(e);
}
}
return null;
}
/**
* Sets the default Privacy list used for the session's user. This list is
* processed if there is no active list set for the session.
*
* @param defaultList the default Privacy list used for the session's user.
*/
public void setDefaultList(PrivacyList defaultList) {
this.defaultList = defaultList != null ? defaultList.getName() : null;
}
/**
* Creates a session with an underlying connection and permission protection.
*
* @param serverName name of the server.
* @param connection The connection we are proxying.
* @param streamID unique identifier of this session.
*/
public LocalClientSession(String serverName, Connection connection, StreamID streamID) {
super(serverName, connection, streamID);
// Set an unavailable initial presence
presence = new Presence();
presence.setType(Presence.Type.unavailable);
}
/**
* Returns the username associated with this session. Use this information
* with the user manager to obtain the user based on username.
*
* @return the username associated with this session
* @throws org.jivesoftware.openfire.user.UserNotFoundException if a user is not associated with a session
* (the session has not authenticated yet)
*/
public String getUsername() throws UserNotFoundException {
if (authToken == null) {
throw new UserNotFoundException();
}
return getAddress().getNode();
}
/**
* Sets the new Authorization Token for this session. The session is not yet considered fully
* authenticated (i.e. active) since a resource has not been binded at this point. This
* message will be sent after SASL authentication was successful but yet resource binding
* is required.
*
* @param auth the authentication token obtained from SASL authentication.
*/
public void setAuthToken(AuthToken auth) {
authToken = auth;
}
/**
* Initialize the session with a valid authentication token and
* resource name. This automatically upgrades the session's
* status to authenticated and enables many features that are not
* available until authenticated (obtaining managers for example).
*
* @param auth the authentication token obtained from the AuthFactory.
* @param resource the resource this session authenticated under.
*/
public void setAuthToken(AuthToken auth, String resource) {
setAddress(new JID(auth.getUsername(), getServerName(), resource));
authToken = auth;
setStatus(Session.STATUS_AUTHENTICATED);
// Set default privacy list for this session
setDefaultList(PrivacyListManager.getInstance().getDefaultPrivacyList(auth.getUsername()));
// Add session to the session manager. The session will be added to the routing table as well
sessionManager.addSession(this);
}
/**
* Initialize the session as an anonymous login. This automatically upgrades the session's
* status to authenticated and enables many features that are not available until
* authenticated (obtaining managers for example).<p>
*/
public void setAnonymousAuth() {
// Anonymous users have a full JID. Use the random resource as the JID's node
String resource = getAddress().getResource();
setAddress(new JID(resource, getServerName(), resource, true));
setStatus(Session.STATUS_AUTHENTICATED);
// Add session to the session manager. The session will be added to the routing table as well
sessionManager.addSession(this);
}
/**
* Returns the authentication token associated with this session.
*
* @return the authentication token associated with this session (can be null).
*/
public AuthToken getAuthToken() {
return authToken;
}
/**
* Flag indicating if this session has been initialized once coming
* online. Session initialization occurs after the session receives
* the first "available" presence update from the client. Initialization
* actions include pushing offline messages, presence subscription requests,
* and presence statuses to the client. Initialization occurs only once
* following the first available presence transition.
*
* @return True if the session has already been initializsed
*/
public boolean isInitialized() {
return initialized;
}
/**
* Sets the initialization state of the session.
*
* @param isInit True if the session has been initialized
* @see #isInitialized
*/
public void setInitialized(boolean isInit) {
initialized = isInit;
}
/**
* Returns true if the session was available ever.
*
* @return true if the session was available ever.
*/
public boolean wasAvailable() {
return wasAvailable;
}
/**
* Returns true if the offline messages of the user should be sent to the user when
* the user becomes online. If the user sent a disco request with node
* "http://jabber.org/protocol/offline" before the available presence then do not
* flood the user with the offline messages. If the user is connected from many resources
* then if one of the sessions stopped the flooding then no session should flood the user.
*
* @return true if the offline messages of the user should be sent to the user when the user
* becomes online.
*/
public boolean canFloodOfflineMessages() {
if(offlineFloodStopped) {
return false;
}
String username = getAddress().getNode();
for (ClientSession session : sessionManager.getSessions(username)) {
if (session.isOfflineFloodStopped()) {
return false;
}
}
return true;
}
/**
* Returns true if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*
* @return true if the user requested to not receive offline messages when sending
* an available presence.
*/
public boolean isOfflineFloodStopped() {
return offlineFloodStopped;
}
/**
* Sets if the user requested to not receive offline messages when sending
* an available presence. The user may send a disco request with node
* "http://jabber.org/protocol/offline" so that no offline messages are sent to the
* user when he becomes online. If the user is connected from many resources then
* if one of the sessions stopped the flooding then no session should flood the user.
*
* @param offlineFloodStopped if the user requested to not receive offline messages when
* sending an available presence.
*/
public void setOfflineFloodStopped(boolean offlineFloodStopped) {
this.offlineFloodStopped = offlineFloodStopped;
}
/**
* Obtain the presence of this session.
*
* @return The presence of this session or null if not authenticated
*/
public Presence getPresence() {
return presence;
}
/**
* Set the presence of this session
*
* @param presence The presence for the session
* @return The old priority of the session or null if not authenticated
*/
public Presence setPresence(Presence presence) {
Presence oldPresence = this.presence;
this.presence = presence;
if (oldPresence.isAvailable() && !this.presence.isAvailable()) {
// The client is no longer available
sessionManager.sessionUnavailable(this);
// Mark that the session is no longer initialized. This means that if the user sends
// an available presence again the session will be initialized again thus receiving
// offline messages and offline presence subscription requests
setInitialized(false);
// Notify listeners that the session is no longer available
PresenceEventDispatcher.unavailableSession(this, presence);
}
else if (!oldPresence.isAvailable() && this.presence.isAvailable()) {
// The client is available
sessionManager.sessionAvailable(this);
wasAvailable = true;
// Notify listeners that the session is now available
PresenceEventDispatcher.availableSession(this, presence);
}
else if (this.presence.isAvailable() && oldPresence.getPriority() != this.presence.getPriority())
{
// The client has changed the priority of his presence
sessionManager.changePriority(this, oldPresence.getPriority());
// Notify listeners that the priority of the session/resource has changed
PresenceEventDispatcher.presencePriorityChanged(this, presence);
}
else if (this.presence.isAvailable()) {
// Notify listeners that the show or status value of the presence has changed
PresenceEventDispatcher.presenceChanged(this, presence);
}
return oldPresence;
}
/**
* Returns the number of conflicts detected on this session.
* Conflicts typically occur when another session authenticates properly
* to the user account and requests to use a resource matching the one
* in use by this session. Administrators may configure the server to automatically
* kick off existing sessions when their conflict count exceeds some limit including
* 0 (old sessions are kicked off immediately to accommodate new sessions). Conflicts
* typically signify the existing (old) session is broken/hung.
*
* @return The number of conflicts detected for this session
*/
public int getConflictCount() {
return conflictCount;
}
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);
// 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 (getAuthToken() == null) {
// Advertise that the server supports Non-SASL Authentication
sb.append("<auth xmlns=\"http://jabber.org/features/iq-auth\"/>");
// Advertise that the server supports In-Band Registration
if (XMPPServer.getInstance().getIQRegisterHandler().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();
}
/**
* Increments the conflict by one.
*/
public void incrementConflictCount() {
conflictCount++;
}
/**
* Returns true if the specified packet must not be blocked based on the active or default
* privacy list rules. The active list will be tried first. If none was found then the
* default list is going to be used. If no default list was defined for this user then
* allow the packet to flow.
*
* @param packet the packet to analyze if it must be blocked.
* @return true if the specified packet must be blocked.
*/
public boolean canProcess(Packet packet) {
PrivacyList list = getActiveList();
if (list != null) {
// If a privacy list is active then make sure that the packet is not blocked
return !list.shouldBlockPacket(packet);
}
else {
list = getDefaultList();
// There is no active list so check if there exists a default list and make
// sure that the packet is not blocked
return list == null || !list.shouldBlockPacket(packet);
}
}
void deliver(Packet packet) throws UnauthorizedException {
if (conn != null && !conn.isClosed()) {
conn.deliver(packet);
}
}
public String toString() {
return super.toString() + " presence: " + presence;
}
}
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 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.openfire.session;
import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.PacketException;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.component.ExternalComponentManager;
import org.jivesoftware.openfire.component.InternalComponentManager;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.component.ComponentManager;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.StreamError;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Represents a session between the server and a component.
*
* @author Gaston Dombiak
*/
public class LocalComponentSession extends LocalSession implements ComponentSession {
private ExternalComponent component;
/**
* Returns a newly created session between the server and a component. The session will be
* created and returned only if all the checkings were correct.<p>
*
* A domain will be binded for the new connecting component. This method is following
* the JEP-114 where the domain to bind is sent in the TO attribute of the stream header.
*
* @param serverName the name of the server where the session is connecting to.
* @param reader the reader that is reading the provided XML through the connection.
* @param connection the connection with the component.
* @return a newly created session between the server and a component.
*/
public static LocalComponentSession createSession(String serverName, XMPPPacketReader reader,
SocketConnection connection) throws UnauthorizedException, IOException,
XmlPullParserException
{
XmlPullParser xpp = reader.getXPPParser();
String domain = xpp.getAttributeValue("", "to");
Log.debug("[ExComp] Starting registration of new external component for domain: " + domain);
Writer writer = connection.getWriter();
// Default answer header in case of an error
StringBuilder sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
sb.append("<stream:stream ");
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
sb.append("xmlns=\"jabber:component:accept\" from=\"");
sb.append(domain);
sb.append("\">");
// Check that a domain was provided in the stream header
if (domain == null) {
Log.debug("[ExComp] Domain not specified in stanza: " + xpp.getText());
// Include the bad-format in the response
StreamError error = new StreamError(StreamError.Condition.bad_format);
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Close the underlying connection
connection.close();
return null;
}
// Get the requested subdomain
String subdomain = domain;
int index = domain.indexOf(serverName);
if (index > -1) {
subdomain = domain.substring(0, index -1);
}
// Check that an external component for the specified subdomain may connect to this server
if (!ExternalComponentManager.canAccess(subdomain)) {
Log.debug("[ExComp] Component is not allowed to connect with subdomain: " + subdomain);
StreamError error = new StreamError(StreamError.Condition.host_unknown);
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Close the underlying connection
connection.close();
return null;
}
// Check that a secret key was configured in the server
String secretKey = ExternalComponentManager.getSecretForComponent(subdomain);
if (secretKey == null) {
Log.debug("[ExComp] A shared secret for the component was not found.");
// Include the internal-server-error in the response
StreamError error = new StreamError(StreamError.Condition.internal_server_error);
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Close the underlying connection
connection.close();
return null;
}
// Check that the requested subdomain is not already in use
if (InternalComponentManager.getInstance().getComponent(subdomain) != null) {
Log.debug("[ExComp] Another component is already using domain: " + domain);
// Domain already occupied so return a conflict error and close the connection
// Include the conflict error in the response
StreamError error = new StreamError(StreamError.Condition.conflict);
sb.append(error.toXML());
writer.write(sb.toString());
writer.flush();
// Close the underlying connection
connection.close();
return null;
}
// Create a ComponentSession for the external component
LocalComponentSession session =
SessionManager.getInstance().createComponentSession(new JID(null, domain, null), connection);
session.component = new LocalExternalComponent(connection);
try {
Log.debug("[ExComp] Send stream header with ID: " + session.getStreamID() +
" for component with domain: " +
domain);
// Build the start packet response
sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
sb.append("<stream:stream ");
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
sb.append("xmlns=\"jabber:component:accept\" from=\"");
sb.append(domain);
sb.append("\" id=\"");
sb.append(session.getStreamID().toString());
sb.append("\">");
writer.write(sb.toString());
writer.flush();
// Perform authentication. Wait for the handshake (with the secret key)
Element doc = reader.parseDocument().getRootElement();
String digest = "handshake".equals(doc.getName()) ? doc.getStringValue() : "";
String anticipatedDigest = AuthFactory.createDigest(session.getStreamID().getID(), secretKey);
// Check that the provided handshake (secret key + sessionID) is correct
if (!anticipatedDigest.equalsIgnoreCase(digest)) {
Log.debug("[ExComp] Incorrect handshake for component with domain: " + domain);
// The credentials supplied by the initiator are not valid (answer an error
// and close the connection)
writer.write(new StreamError(StreamError.Condition.not_authorized).toXML());
writer.flush();
// Close the underlying connection
connection.close();
return null;
}
else {
// Component has authenticated fine
session.setStatus(STATUS_AUTHENTICATED);
// Send empty handshake element to acknowledge success
writer.write("<handshake></handshake>");
writer.flush();
// Bind the domain to this component
ExternalComponent component = session.getExternalComponent();
InternalComponentManager.getInstance().addComponent(subdomain, component);
Log.debug("[ExComp] External component was registered SUCCESSFULLY with domain: " + domain);
return session;
}
}
catch (Exception e) {
Log.error("An error occured while creating a ComponentSession", e);
// Close the underlying connection
connection.close();
return null;
}
}
public LocalComponentSession(String serverName, Connection conn, StreamID id) {
super(serverName, conn, id);
}
public String getAvailableStreamFeatures() {
// Nothing special to add
return null;
}
boolean canProcess(Packet packet) {
return true;
}
void deliver(Packet packet) throws PacketException {
// Since ComponentSessions are not being stored in the RoutingTable this messages is very
// unlikely to be sent
component.processPacket(packet);
}
public ExternalComponent getExternalComponent() {
return component;
}
/**
* The ExternalComponent acts as a proxy of the remote connected component. Any Packet that is
* sent to this component will be delivered to the real component on the other side of the
* connection.<p>
*
* An ExternalComponent will be added as a route in the RoutingTable for each connected
* external component. This implies that when the server receives a packet whose domain matches
* the external component services address then a route to the external component will be used
* and the packet will be forwarded to the component on the other side of the connection.
*
* @author Gaston Dombiak
*/
public static class LocalExternalComponent implements ComponentSession.ExternalComponent {
private Connection connection;
private String name = "";
private String type = "";
private String category = "";
/**
* List of subdomains that were binded for this component. The list will include
* the initial subdomain.
*/
private List<String> subdomains = new ArrayList<String>();
public LocalExternalComponent(Connection connection) {
this.connection = connection;
}
public void processPacket(Packet packet) {
if (connection != null && !connection.isClosed()) {
try {
connection.deliver(packet);
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
connection.close();
}
}
}
public String getName() {
return name;
}
public String getDescription() {
return category + " - " + type;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getInitialSubdomain() {
if (subdomains.isEmpty()) {
return null;
}
return subdomains.get(0);
}
private void addSubdomain(String subdomain) {
subdomains.add(subdomain);
}
public Collection<String> getSubdomains() {
return subdomains;
}
public void initialize(JID jid, ComponentManager componentManager) {
addSubdomain(jid.toString());
}
public void start() {
}
public void shutdown() {
}
public String toString() {
return super.toString() + " - subdomains: " + subdomains;
}
}
}
package org.jivesoftware.openfire.session;
import org.dom4j.Element;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthFactory;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.multiplex.ConnectionMultiplexerManager;
import org.jivesoftware.openfire.multiplex.MultiplexerPacketDeliverer;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.StreamError;
import java.util.Collection;
/**
* Represents a session between the server and a connection manager.<p>
*
* Each Connection Manager has its own domain. Each connection from the same connection manager
* uses a different resource. Unlike any other session, connection manager sessions are not
* present in the routing table. This means that connection managers are not reachable entities.
* In other words, entities cannot send packets to connection managers but clients being hosted
* by them. The main reason behind this design decision is that connection managers are private
* components of the server so they can only be contacted by the server. Connection Manager
* sessions are present in {@link SessionManager} but not in {@link org.jivesoftware.openfire.RoutingTable}. Use
* {@link SessionManager#getConnectionMultiplexerSessions(String)} to get all sessions or
* {@link org.jivesoftware.openfire.multiplex.ConnectionMultiplexerManager#getMultiplexerSession(String)}
* to get a random session to a given connection manager.
*
* @author Gaston Dombiak
*/
public class LocalConnectionMultiplexerSession extends LocalSession implements ConnectionMultiplexerSession {
private static Connection.TLSPolicy tlsPolicy;
private static Connection.CompressionPolicy compressionPolicy;
static {
// Set the TLS policy stored as a system property
String policyName = JiveGlobals.getProperty("xmpp.multiplex.tls.policy",
Connection.TLSPolicy.disabled.toString());
tlsPolicy = Connection.TLSPolicy.valueOf(policyName);
// Set the Compression policy stored as a system property
policyName = JiveGlobals.getProperty("xmpp.multiplex.compression.policy",
Connection.CompressionPolicy.disabled.toString());
compressionPolicy = Connection.CompressionPolicy.valueOf(policyName);
}
public static LocalConnectionMultiplexerSession createSession(String serverName, XmlPullParser xpp, Connection connection)
throws XmlPullParserException {
String domain = xpp.getAttributeValue("", "to");
Log.debug("[ConMng] Starting registration of new connection manager for domain: " + domain);
// Default answer header in case of an error
StringBuilder sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
sb.append("<stream:stream ");
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
sb.append("xmlns=\"jabber:connectionmanager\" from=\"");
sb.append(domain);
sb.append("\" version=\"1.0\">");
// Check that a domain was provided in the stream header
if (domain == null) {
Log.debug("[ConMng] Domain not specified in stanza: " + xpp.getText());
// Include the bad-format in the response
StreamError error = new StreamError(StreamError.Condition.bad_format);
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
return null;
}
// Get the requested domain
JID address = new JID(domain);
// Check that a secret key was configured in the server
String secretKey = ConnectionMultiplexerManager.getDefaultSecret();
if (secretKey == null) {
Log.debug("[ConMng] A shared secret for connection manager was not found.");
// Include the internal-server-error in the response
StreamError error = new StreamError(StreamError.Condition.internal_server_error);
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
return null;
}
// Check that the requested subdomain is not already in use
if (SessionManager.getInstance().getConnectionMultiplexerSession(address) != null) {
Log.debug("[ConMng] Another connection manager is already using domain: " + domain);
// Domain already occupied so return a conflict error and close the connection
// Include the conflict error in the response
StreamError error = new StreamError(StreamError.Condition.conflict);
sb.append(error.toXML());
connection.deliverRawText(sb.toString());
// Close the underlying connection
connection.close();
return null;
}
// Indicate the TLS policy to use for this connection
connection.setTlsPolicy(tlsPolicy);
// Indicate the compression policy to use for this connection
connection.setCompressionPolicy(compressionPolicy);
// Set the connection manager domain to use delivering a packet fails
((MultiplexerPacketDeliverer) connection.getPacketDeliverer())
.setConnectionManagerDomain(address.getDomain());
// Create a ConnectionMultiplexerSession for the new session originated
// from the connection manager
LocalConnectionMultiplexerSession session =
SessionManager.getInstance().createMultiplexerSession(connection, address);
// Set the address of the new session
session.setAddress(address);
connection.init(session);
try {
Log.debug("[ConMng] Send stream header with ID: " + session.getStreamID() +
" for connection manager with domain: " +
domain);
// Build the start packet response
sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='");
sb.append(CHARSET);
sb.append("'?>");
sb.append("<stream:stream ");
sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" ");
sb.append("xmlns=\"jabber:connectionmanager\" from=\"");
sb.append(domain);
sb.append("\" id=\"");
sb.append(session.getStreamID().toString());
sb.append("\" version=\"1.0\" >");
connection.deliverRawText(sb.toString());
// Announce stream features.
sb = new StringBuilder(490);
sb.append("<stream:features>");
if (tlsPolicy != Connection.TLSPolicy.disabled) {
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (tlsPolicy == Connection.TLSPolicy.required) {
sb.append("<required/>");
}
sb.append("</starttls>");
}
// Include Stream features
String specificFeatures = session.getAvailableStreamFeatures();
if (specificFeatures != null) {
sb.append(specificFeatures);
}
sb.append("</stream:features>");
connection.deliverRawText(sb.toString());
return session;
}
catch (Exception e) {
Log.error("An error occured while creating a Connection Manager Session", e);
// Close the underlying connection
connection.close();
return null;
}
}
public LocalConnectionMultiplexerSession(String serverName, Connection connection, StreamID streamID) {
super(serverName, connection, streamID);
}
public String getAvailableStreamFeatures() {
if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) {
return null;
}
// Include Stream Compression Mechanism
if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
return "<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>";
}
return null;
}
/**
* Authenticates the connection manager. Shared secret is validated with the one provided
* by the connection manager. If everything went fine then the session will have a status
* of "authenticated" and the connection manager will receive the client configuration
* options.
*
* @param digest the digest provided by the connection manager with the handshake stanza.
* @return true if the connection manager was sucessfully authenticated.
*/
public boolean authenticate(String digest) {
// Perform authentication. Wait for the handshake (with the secret key)
String anticipatedDigest = AuthFactory.createDigest(getStreamID().getID(),
ConnectionMultiplexerManager.getDefaultSecret());
// Check that the provided handshake (secret key + sessionID) is correct
if (!anticipatedDigest.equalsIgnoreCase(digest)) {
Log.debug("[ConMng] Incorrect handshake for connection manager with domain: " +
getAddress().getDomain());
// The credentials supplied by the initiator are not valid (answer an error
// and close the connection)
conn.deliverRawText(new StreamError(StreamError.Condition.not_authorized).toXML());
// Close the underlying connection
conn.close();
return false;
}
else {
// Component has authenticated fine
setStatus(STATUS_AUTHENTICATED);
// Send empty handshake element to acknowledge success
conn.deliverRawText("<handshake></handshake>");
Log.debug("[ConMng] Connection manager was AUTHENTICATED with domain: " + getAddress());
sendClientOptions();
return true;
}
}
/**
* Send to the Connection Manager the connection options available for clients. The info
* to send includes:
* <ul>
* <li>if TLS is available, optional or required
* <li>SASL mechanisms available before TLS is negotiated
* <li>if compression is available
* <li>if Non-SASL authentication is available
* <li>if In-Band Registration is available
* </ul
*/
private void sendClientOptions() {
IQ options = new IQ(IQ.Type.set);
Element child = options.setChildElement("configuration",
"http://jabber.org/protocol/connectionmanager");
// Add info about TLS
if (LocalClientSession.getTLSPolicy() != Connection.TLSPolicy.disabled) {
Element tls = child.addElement("starttls", "urn:ietf:params:xml:ns:xmpp-tls");
if (LocalClientSession.getTLSPolicy() == Connection.TLSPolicy.required) {
tls.addElement("required");
}
}
// Add info about SASL mechanisms
Collection<String> mechanisms = SASLAuthentication.getSupportedMechanisms();
if (!mechanisms.isEmpty()) {
Element sasl = child.addElement("mechanisms", "urn:ietf:params:xml:ns:xmpp-sasl");
for (String mechanism : mechanisms) {
sasl.addElement("mechanism").setText(mechanism);
}
}
// Add info about Stream Compression
if (LocalClientSession.getCompressionPolicy() == Connection.CompressionPolicy.optional) {
Element comp = child.addElement("compression", "http://jabber.org/features/compress");
comp.addElement("method").setText("zlib");
}
// Add info about Non-SASL authentication
child.addElement("auth", "http://jabber.org/features/iq-auth");
// Add info about In-Band Registration
if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
child.addElement("register", "http://jabber.org/features/iq-register");
}
// Send the options
process(options);
}
boolean canProcess(Packet packet) {
return true;
}
void deliver(Packet packet) throws UnauthorizedException {
if (conn != null && !conn.isClosed()) {
conn.deliver(packet);
}
}
/**
* Returns whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @return whether TLS is mandatory, optional or is disabled.
*/
public static SocketConnection.TLSPolicy getTLSPolicy() {
return tlsPolicy;
}
/**
* Sets whether TLS is mandatory, optional or is disabled for clients. When TLS is
* mandatory clients are required to secure their connections or otherwise their connections
* will be closed. On the other hand, when TLS is disabled clients are not allowed to secure
* their connections using TLS. Their connections will be closed if they try to secure the
* connection. in this last case.
*
* @param policy whether TLS is mandatory, optional or is disabled.
*/
public static void setTLSPolicy(SocketConnection.TLSPolicy policy) {
tlsPolicy = policy;
JiveGlobals.setProperty("xmpp.multiplex.tls.policy", tlsPolicy.toString());
}
/**
* Returns whether compression is optional or is disabled for clients.
*
* @return whether compression is optional or is disabled.
*/
public static SocketConnection.CompressionPolicy getCompressionPolicy() {
return compressionPolicy;
}
/**
* Sets whether compression is optional or is disabled for clients.
*
* @param policy whether compression is optional or is disabled.
*/
public static void setCompressionPolicy(SocketConnection.CompressionPolicy policy) {
compressionPolicy = policy;
JiveGlobals.setProperty("xmpp.multiplex.compression.policy", compressionPolicy.toString());
}
}
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 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.openfire.session;
import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.net.SSLConfig;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.server.ServerDialback;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.Packet;
import org.xmpp.packet.StreamError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
/**
* Server-to-server communication is done using two TCP connections between the servers. One
* connection is used for sending packets while the other connection is used for receiving packets.
* The <tt>IncomingServerSession</tt> represents the connection to a remote server that will only
* be used for receiving packets.<p>
*
* Currently only the Server Dialback method is being used for authenticating the remote server.
* Once the remote server has been authenticated incoming packets will be processed by this server.
* It is also possible for remote servers to authenticate more domains once the session has been
* established. For optimization reasons the existing connection is used between the servers.
* Therefore, the incoming server session holds the list of authenticated domains which are allowed
* to send packets to this server.<p>
*
* Using the Server Dialback method it is possible that this server may also act as the
* Authoritative Server. This implies that an incoming connection will be established with this
* server for authenticating a domain. This incoming connection will only last for a brief moment
* and after the domain has been authenticated the connection will be closed and no session will
* exist.
*
* @author Gaston Dombiak
*/
public class LocalIncomingServerSession extends LocalSession implements IncomingServerSession {
/**
* List of domains, subdomains and virtual hostnames of the remote server that were
* validated with this server. The remote server is allowed to send packets to this
* server from any of the validated domains.
*/
private Collection<String> validatedDomains = new ArrayList<String>();
/**
* Domains or subdomain of this server that was used by the remote server
* when validating the new connection. This information is useful to prevent
* many connections from the same remote server to the same local domain.
*/
private String localDomain = null;
/**
* Creates a new session that will receive packets. The new session will be authenticated
* before being returned. If the authentication process fails then the answer will be
* <tt>null</tt>.<p>
*
* Currently the Server Dialback method is the only way to authenticate a remote server. Since
* Server Dialback requires an Authoritative Server, it is possible for this server to receive
* an incoming connection that will only exist until the requested domain has been validated.
* In this case, this method will return <tt>null</tt> since the connection is closed after
* the domain was validated. See
* {@link org.jivesoftware.openfire.server.ServerDialback#createIncomingSession(org.dom4j.io.XMPPPacketReader)} for more
* information.
*
* @param serverName hostname of this server.
* @param reader reader on the new established connection with the remote server.
* @param connection the new established connection with the remote server.
* @return a new session that will receive packets or null if a problem occured while
* authenticating the remote server or when acting as the Authoritative Server during
* a Server Dialback authentication process.
* @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing the XML.
* @throws java.io.IOException if an input/output error occurs while using the connection.
*/
public static LocalIncomingServerSession createSession(String serverName, XMPPPacketReader reader,
SocketConnection connection) throws XmlPullParserException, IOException {
XmlPullParser xpp = reader.getXPPParser();
if (xpp.getNamespace("db") != null) {
// Server is trying to establish connection and authenticate using server dialback
if (ServerDialback.isEnabled()) {
ServerDialback method = new ServerDialback(connection, serverName);
return method.createIncomingSession(reader);
}
Log.debug("Server dialback is disabled. Rejecting connection: " + connection);
}
String version = xpp.getAttributeValue("", "version");
int[] serverVersion = version != null ? decodeVersion(version) : new int[] {0,0};
if (serverVersion[0] >= 1) {
// Remote server is XMPP 1.0 compliant so offer TLS and SASL to establish the connection
if (JiveGlobals.getBooleanProperty("xmpp.server.tls.enabled", true)) {
try {
return createIncomingSession(connection, serverName);
}
catch (Exception e) {
Log.error("Error establishing connection from remote server", e);
}
}
else {
connection.deliverRawText(
new StreamError(StreamError.Condition.invalid_namespace).toXML());
Log.debug("Server TLS is disabled. Rejecting connection: " + connection);
}
}
// Close the connection since remote server is not XMPP 1.0 compliant and is not
// using server dialback to establish and authenticate the connection
connection.close();
return null;
}
/**
* Returns a new incoming server session pending to be authenticated. The remote server
* will be notified that TLS and SASL are available. The remote server will be able to
* send packets to this server only after SASL authentication has been finished.
*
* @param connection the new established connection with the remote server.
* @param serverName hostname of this server.
* @return a new incoming server session pending to be authenticated.
* @throws org.jivesoftware.openfire.auth.UnauthorizedException if this server is being shutdown.
*/
private static LocalIncomingServerSession createIncomingSession(SocketConnection connection, String serverName)
throws UnauthorizedException {
// Get the stream ID for the new session
StreamID streamID = SessionManager.getInstance().nextStreamID();
// Create a server Session for the remote server
LocalIncomingServerSession session =
SessionManager.getInstance().createIncomingServerSession(connection, streamID);
// Send the stream header
StringBuilder openingStream = new StringBuilder();
openingStream.append("<stream:stream");
openingStream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
openingStream.append(" xmlns=\"jabber:server\"");
openingStream.append(" from=\"").append(serverName).append("\"");
openingStream.append(" id=\"").append(streamID).append("\"");
openingStream.append(" version=\"1.0\">");
connection.deliverRawText(openingStream.toString());
// Indicate the TLS policy to use for this connection
Connection.TLSPolicy tlsPolicy =
ServerDialback.isEnabled() ? Connection.TLSPolicy.optional :
Connection.TLSPolicy.required;
boolean hasCertificates = false;
try {
hasCertificates = SSLConfig.getKeyStore().size() > 0;
}
catch (Exception e) {
Log.error(e);
}
if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
Log.error("Server session rejected. TLS is required but no certificates " +
"were created.");
return null;
}
connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
// Indicate the compression policy to use for this connection
String policyName = JiveGlobals.getProperty("xmpp.server.compression.policy",
Connection.CompressionPolicy.disabled.toString());
Connection.CompressionPolicy compressionPolicy =
Connection.CompressionPolicy.valueOf(policyName);
connection.setCompressionPolicy(compressionPolicy);
StringBuilder sb = new StringBuilder();
sb.append("<stream:features>");
sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
if (!ServerDialback.isEnabled()) {
// Server dialback is disabled so TLS is required
sb.append("<required/>");
}
sb.append("</starttls>");
// Include available SASL Mechanisms
sb.append(SASLAuthentication.getSASLMechanisms(session));
sb.append("</stream:features>");
connection.deliverRawText(sb.toString());
// Set the domain or subdomain of the local server targeted by the remote server
session.setLocalDomain(serverName);
return session;
}
public LocalIncomingServerSession(String serverName, Connection connection, StreamID streamID) {
super(serverName, connection, streamID);
}
boolean canProcess(Packet packet) {
return true;
}
void deliver(Packet packet) throws UnauthorizedException {
// Do nothing
}
/**
* Returns true if the request of a new domain was valid. Sessions may receive subsequent
* domain validation request. If the validation of the new domain fails then the session and
* the underlying TCP connection will be closed.<p>
*
* For optimization reasons, the same session may be servicing several domains of a
* remote server.
*
* @param dbResult the DOM stanza requesting the domain validation.
* @return true if the requested domain was valid.
*/
public boolean validateSubsequentDomain(Element dbResult) {
ServerDialback method = new ServerDialback(getConnection(), getServerName());
if (method.validateRemoteDomain(dbResult, getStreamID())) {
// Add the validated domain as a valid domain
addValidatedDomain(dbResult.attributeValue("from"));
return true;
}
return false;
}
/**
* Returns true if the specified domain has been validated for this session. The remote
* server should send a "db:result" packet for registering new subdomains or even
* virtual hosts.<p>
*
* In the spirit of being flexible we allow remote servers to not register subdomains
* and even so consider subdomains that include the server domain in their domain part
* as valid domains.
*
* @param domain the domain to validate.
* @return true if the specified domain has been validated for this session.
*/
public boolean isValidDomain(String domain) {
// Check if the specified domain is contained in any of the validated domains
for (String validatedDomain : getValidatedDomains()) {
if (domain.contains(validatedDomain)) {
return true;
}
}
return false;
}
/**
* Returns a collection with all the domains, subdomains and virtual hosts that where
* validated. The remote server is allowed to send packets from any of these domains,
* subdomains and virtual hosts.
*
* @return domains, subdomains and virtual hosts that where validated.
*/
public Collection<String> getValidatedDomains() {
return Collections.unmodifiableCollection(validatedDomains);
}
/**
* Adds a new validated domain, subdomain or virtual host to the list of
* validated domains for the remote server.
*
* @param domain the new validated domain, subdomain or virtual host to add.
*/
public void addValidatedDomain(String domain) {
if (validatedDomains.add(domain)) {
// Register the new validated domain for this server session in SessionManager
SessionManager.getInstance().registerIncomingServerSession(domain, this);
}
}
/**
* Removes the previously validated domain from the list of validated domains. The remote
* server will no longer be able to send packets from the removed domain, subdomain or
* virtual host.
*
* @param domain the domain, subdomain or virtual host to remove from the list of
* validated domains.
*/
public void removeValidatedDomain(String domain) {
validatedDomains.remove(domain);
// Unregister the validated domain for this server session in SessionManager
SessionManager.getInstance().unregisterIncomingServerSessions(domain);
}
/**
* Returns the domain or subdomain of the local server used by the remote server
* when validating the session. This information is only used to prevent many
* connections from the same remote server to the same domain or subdomain of
* the local server.
*
* @return the domain or subdomain of the local server used by the remote server
* when validating the session.
*/
public String getLocalDomain() {
return localDomain;
}
/**
* Sets the domain or subdomain of the local server used by the remote server when asking
* to validate the session. This information is only used to prevent many connections from
* the same remote server to the same domain or subdomain of the local server.
*
* @param domain the domain or subdomain of the local server used when validating the
* session.
*/
public void setLocalDomain(String domain) {
localDomain = domain;
}
/**
* Verifies the received key sent by the remote server. This server is trying to generate
* an outgoing connection to the remote server and the remote server is reusing an incoming
* connection for validating the key.
*
* @param doc the received Element that contains the key to verify.
*/
public void verifyReceivedKey(Element doc) {
ServerDialback.verifyReceivedKey(doc, getConnection());
}
public String getAvailableStreamFeatures() {
// Include Stream Compression Mechanism
if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
!conn.isCompressed()) {
return "<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>";
}
// Nothing special to add
return null;
}
}
/**
* $RCSfile: $
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 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.openfire.session;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZInputStream;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.XMPPPacketReader;
import org.jivesoftware.openfire.*;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.net.DNSUtil;
import org.jivesoftware.openfire.net.MXParser;
import org.jivesoftware.openfire.net.SocketConnection;
import org.jivesoftware.openfire.server.OutgoingServerSocketReader;
import org.jivesoftware.openfire.server.RemoteServerConfiguration;
import org.jivesoftware.openfire.server.RemoteServerManager;
import org.jivesoftware.openfire.server.ServerDialback;
import org.jivesoftware.openfire.spi.BasicStreamIDFactory;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.*;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.regex.Pattern;
/**
* Server-to-server communication is done using two TCP connections between the servers. One
* connection is used for sending packets while the other connection is used for receiving packets.
* The <tt>OutgoingServerSession</tt> represents the connection to a remote server that will only
* be used for sending packets.<p>
*
* Currently only the Server Dialback method is being used for authenticating with the remote
* server. Use {@link #authenticateDomain(String, String)} to create a new connection to a remote
* server that will be used for sending packets to the remote server from the specified domain.
* Only the authenticated domains with the remote server will be able to effectively send packets
* to the remote server. The remote server will reject and close the connection if a
* non-authenticated domain tries to send a packet through this connection.<p>
*
* Once the connection has been established with the remote server and at least a domain has been
* authenticated then a new route will be added to the routing table for this connection. For
* optimization reasons the same outgoing connection will be used even if the remote server has
* several hostnames. However, different routes will be created in the routing table for each
* hostname of the remote server.
*
* @author Gaston Dombiak
*/
public class LocalOutgoingServerSession extends LocalSession implements OutgoingServerSession {
/**
* Regular expression to ensure that the hostname contains letters.
*/
private static Pattern pattern = Pattern.compile("[a-zA-Z]");
private Collection<String> authenticatedDomains = new ArrayList<String>();
private Collection<String> hostnames = new ArrayList<String>();
private OutgoingServerSocketReader socketReader;
/**
* Flag that indicates if the session was created usign server-dialback.
*/
private boolean usingServerDialback = true;
/**
* Creates a new outgoing connection to the specified hostname if no one exists. The port of
* the remote server could be configured by setting the <b>xmpp.server.socket.remotePort</b>
* property or otherwise the standard port 5269 will be used. Either a new connection was
* created or already existed the specified hostname will be authenticated with the remote
* server. Once authenticated the remote server will start accepting packets from the specified
* domain.<p>
*
* The Server Dialback method is currently the only implemented method for server-to-server
* authentication. This implies that the remote server will ask the Authoritative Server
* to verify the domain to authenticate. Most probably this server will act as the
* Authoritative Server. See {@link IncomingServerSession} for more information.
*
* @param domain the local domain to authenticate with the remote server.
* @param hostname the hostname of the remote server.
* @return True if the domain was authenticated by the remote server.
*/
public static boolean authenticateDomain(String domain, String hostname) {
if (hostname == null || hostname.length() == 0 || hostname.trim().indexOf(' ') > -1) {
// Do nothing if the target hostname is empty, null or contains whitespaces
return false;
}
try {
// Check if the remote hostname is in the blacklist
if (!RemoteServerManager.canAccess(hostname)) {
return false;
}
OutgoingServerSession session;
// Check if a session, that is using server dialback, already exists to the desired
// hostname (i.e. remote server). If no one exists then create a new session. The same
// session will be used for the same hostname for all the domains to authenticate
SessionManager sessionManager = SessionManager.getInstance();
session = sessionManager.getOutgoingServerSession(hostname);
if (session == null) {
// Try locating if the remote server has previously authenticated with this server
for (IncomingServerSession incomingSession : sessionManager.getIncomingServerSessions(hostname)) {
for (String otherHostname : incomingSession.getValidatedDomains()) {
session = sessionManager.getOutgoingServerSession(otherHostname);
if (session != null) {
if (session.isUsingServerDialback()) {
// A session to the same remote server but with different hostname
// was found. Use this session and add the new hostname to the
// session
session.addHostname(hostname);
break;
} else {
session = null;
}
}
}
}
}
if (session == null) {
int port = RemoteServerManager.getPortForServer(hostname);
// No session was found to the remote server so make sure that only one is created
session = sessionManager.getOutgoingServerSession(hostname);
if (session == null) {
session = createOutgoingSession(domain, hostname, port);
if (session != null) {
// Add the new hostname to the list of names that the server may have
session.addHostname(hostname);
// Add the validated domain as an authenticated domain
session.addAuthenticatedDomain(domain);
// Notify the SessionManager that a new session has been created
sessionManager.outgoingServerSessionCreated((LocalOutgoingServerSession) session);
return true;
} else {
// Ensure that the hostname is not an IP address (i.e. contains chars)
if (!pattern.matcher(hostname).find()) {
return false;
}
// Check if hostname is a subdomain of an existing outgoing session
for (String otherHost : sessionManager.getOutgoingServers()) {
if (hostname.contains(otherHost)) {
session = sessionManager.getOutgoingServerSession(otherHost);
// Add the new hostname to the found session
session.addHostname(hostname);
return true;
}
}
// Try to establish a connection to candidate hostnames. Iterate on the
// substring after the . and try to establish a connection. If a
// connection is established then the same session will be used for
// sending packets to the "candidate hostname" as well as for the
// requested hostname (i.e. the subdomain of the candidate hostname)
// This trick is useful when remote servers haven't registered in their
// DNSs an entry for their subdomains
int index = hostname.indexOf('.');
while (index > -1 && index < hostname.length()) {
String newHostname = hostname.substring(index + 1);
String serverName = XMPPServer.getInstance().getServerInfo().getName();
if ("com".equals(newHostname) || "net".equals(newHostname) ||
"org".equals(newHostname) ||
"gov".equals(newHostname) ||
"edu".equals(newHostname) ||
serverName.equals(newHostname)) {
return false;
}
session = createOutgoingSession(domain, newHostname, port);
if (session != null) {
// Add the new hostname to the list of names that the server may have
session.addHostname(hostname);
// Add the validated domain as an authenticated domain
session.addAuthenticatedDomain(domain);
// Notify the SessionManager that a new session has been created
sessionManager.outgoingServerSessionCreated((LocalOutgoingServerSession) session);
// Add the new hostname to the found session
session.addHostname(newHostname);
return true;
} else {
index = hostname.indexOf('.', index + 1);
}
}
return false;
}
}
}
// A session already exists. The session was established using server dialback so
// it is possible to do piggybacking to authenticate more domains
if (session.getAuthenticatedDomains().contains(domain)) {
// Do nothing since the domain has already been authenticated
return true;
}
// A session already exists so authenticate the domain using that session
return session.authenticateSubdomain(domain, hostname);
}
catch (Exception e) {
Log.error("Error authenticating domain with remote server: " + hostname, e);
}
return false;
}
/**
* Establishes a new outgoing session to a remote server. If the remote server supports TLS
* and SASL then the new outgoing connection will be secured with TLS and authenticated
* using SASL. However, if TLS or SASL is not supported by the remote server or if an
* error occured while securing or authenticating the connection using SASL then server
* dialback method will be used.
*
* @param domain the local domain to authenticate with the remote server.
* @param hostname the hostname of the remote server.
* @param port default port to use to establish the connection.
* @return new outgoing session to a remote server.
*/
private static LocalOutgoingServerSession createOutgoingSession(String domain, String hostname,
int port) {
boolean useTLS = JiveGlobals.getBooleanProperty("xmpp.server.tls.enabled", true);
RemoteServerConfiguration configuration = RemoteServerManager.getConfiguration(hostname);
if (configuration != null) {
// TODO Use the specific TLS configuration for this remote server
//useTLS = configuration.isTLSEnabled();
}
if (useTLS) {
// Connect to remote server using TLS + SASL
SocketConnection connection = null;
String realHostname = null;
int realPort = port;
Socket socket = new Socket();
try {
// Get the real hostname to connect to using DNS lookup of the specified hostname
DNSUtil.HostAddress address = DNSUtil.resolveXMPPServerDomain(hostname, port);
realHostname = address.getHost();
realPort = address.getPort();
Log.debug("OS - Trying to connect to " + hostname + ":" + port +
"(DNS lookup: " + realHostname + ":" + realPort + ")");
// Establish a TCP connection to the Receiving Server
socket.connect(new InetSocketAddress(realHostname, realPort),
RemoteServerManager.getSocketTimeout());
Log.debug("OS - Plain connection to " + hostname + ":" + port + " successful");
}
catch (Exception e) {
Log.error("Error trying to connect to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
return null;
}
try {
connection =
new SocketConnection(XMPPServer.getInstance().getPacketDeliverer(), socket,
false);
// Send the stream header
StringBuilder openingStream = new StringBuilder();
openingStream.append("<stream:stream");
openingStream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
openingStream.append(" xmlns=\"jabber:server\"");
openingStream.append(" to=\"").append(hostname).append("\"");
openingStream.append(" version=\"1.0\">");
connection.deliverRawText(openingStream.toString());
// Set a read timeout (of 5 seconds) so we don't keep waiting forever
int soTimeout = socket.getSoTimeout();
socket.setSoTimeout(5000);
XMPPPacketReader reader = new XMPPPacketReader();
reader.getXPPParser().setInput(new InputStreamReader(socket.getInputStream(),
CHARSET));
// Get the answer from the Receiving Server
XmlPullParser xpp = reader.getXPPParser();
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
String serverVersion = xpp.getAttributeValue("", "version");
// Check if the remote server is XMPP 1.0 compliant
if (serverVersion != null && decodeVersion(serverVersion)[0] >= 1) {
// Restore default timeout
socket.setSoTimeout(soTimeout);
// Get the stream features
Element features = reader.parseDocument().getRootElement();
// Check if TLS is enabled
if (features != null && features.element("starttls") != null) {
// Secure the connection with TLS and authenticate using SASL
LocalOutgoingServerSession answer;
answer = secureAndAuthenticate(hostname, connection, reader, openingStream,
domain);
if (answer != null) {
// Everything went fine so return the secured and
// authenticated connection
return answer;
}
}
else {
Log.debug("OS - Error, <starttls> was not received");
}
}
// Something went wrong so close the connection and try server dialback over
// a plain connection
if (connection != null) {
connection.close();
}
}
catch (SSLHandshakeException e) {
Log.debug("Handshake error while creating secured outgoing session to remote " +
"server: " + hostname + "(DNS lookup: " + realHostname + ":" + realPort +
")", e);
// Close the connection
if (connection != null) {
connection.close();
}
}
catch (XmlPullParserException e) {
Log.warn("Error creating secured outgoing session to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
// Close the connection
if (connection != null) {
connection.close();
}
}
catch (Exception e) {
Log.error("Error creating secured outgoing session to remote server: " + hostname +
"(DNS lookup: " + realHostname + ":" + realPort + ")", e);
// Close the connection
if (connection != null) {
connection.close();
}
}
}
if (ServerDialback.isEnabled()) {
Log.debug("OS - Going to try connecting using server dialback with: " + hostname);
// Use server dialback over a plain connection
return new ServerDialback().createOutgoingSession(domain, hostname, port);
}
return null;
}
private static LocalOutgoingServerSession secureAndAuthenticate(String hostname,
SocketConnection connection, XMPPPacketReader reader, StringBuilder openingStream,
String domain) throws Exception {
Element features;
Log.debug("OS - Indicating we want TLS to " + hostname);
connection.deliverRawText("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
MXParser xpp = reader.getXPPParser();
// Wait for the <proceed> response
Element proceed = reader.parseDocument().getRootElement();
if (proceed != null && proceed.getName().equals("proceed")) {
Log.debug("OS - Negotiating TLS with " + hostname);
connection.startTLS(true, hostname);
Log.debug("OS - TLS negotiation with " + hostname + " was successful");
// TLS negotiation was successful so initiate a new stream
connection.deliverRawText(openingStream.toString());
// Reset the parser to use the new secured reader
xpp.setInput(new InputStreamReader(connection.getTLSStreamHandler().getInputStream(),
CHARSET));
// Skip new stream element
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
// Get new stream features
features = reader.parseDocument().getRootElement();
if (features != null && features.element("mechanisms") != null) {
// Check if we can use stream compression
String policyName = JiveGlobals.getProperty("xmpp.server.compression.policy",
Connection.CompressionPolicy.disabled.toString());
Connection.CompressionPolicy compressionPolicy =
Connection.CompressionPolicy.valueOf(policyName);
if (Connection.CompressionPolicy.optional == compressionPolicy) {
// Verify if the remote server supports stream compression
Element compression = features.element("compression");
if (compression != null) {
boolean zlibSupported = false;
Iterator it = compression.elementIterator("method");
while (it.hasNext()) {
Element method = (Element) it.next();
if ("zlib".equals(method.getTextTrim())) {
zlibSupported = true;
}
}
if (zlibSupported) {
// Request Stream Compression
connection.deliverRawText("<compress xmlns='http://jabber.org/protocol/compress'><method>zlib</method></compress>");
// Check if we are good to start compression
Element answer = reader.parseDocument().getRootElement();
if ("compressed".equals(answer.getName())) {
// Server confirmed that we can use zlib compression
connection.addCompression();
connection.startCompression();
Log.debug("OS - Stream compression was successful with " + hostname);
// Stream compression was successful so initiate a new stream
connection.deliverRawText(openingStream.toString());
// Reset the parser to use stream compression over TLS
ZInputStream in = new ZInputStream(
connection.getTLSStreamHandler().getInputStream());
in.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
xpp.setInput(new InputStreamReader(in, CHARSET));
// Skip the opening stream sent by the server
for (int eventType = xpp.getEventType(); eventType != XmlPullParser.START_TAG;)
{
eventType = xpp.next();
}
// Get new stream features
features = reader.parseDocument().getRootElement();
if (features == null || features.element("mechanisms") == null) {
Log.debug("OS - Error, EXTERNAL SASL was not offered by " + hostname);
return null;
}
}
else {
Log.debug("OS - Stream compression was rejected by " + hostname);
}
}
else {
Log.debug(
"OS - Stream compression found but zlib method is not supported by" +
hostname);
}
}
else {
Log.debug("OS - Stream compression not supoprted by " + hostname);
}
}
Iterator it = features.element("mechanisms").elementIterator();
while (it.hasNext()) {
Element mechanism = (Element) it.next();
if ("EXTERNAL".equals(mechanism.getTextTrim())) {
Log.debug("OS - Starting EXTERNAL SASL with " + hostname);
if (doExternalAuthentication(domain, connection, reader)) {
Log.debug("OS - EXTERNAL SASL with " + hostname + " was successful");
// SASL was successful so initiate a new stream
connection.deliverRawText(openingStream.toString());
// Reset the parser
xpp.resetInput();
// Skip the opening stream sent by the server
for (int eventType = xpp.getEventType();
eventType != XmlPullParser.START_TAG;) {
eventType = xpp.next();
}
// SASL authentication was successful so create new
// OutgoingServerSession
String id = xpp.getAttributeValue("", "id");
StreamID streamID = new BasicStreamIDFactory().createStreamID(id);
LocalOutgoingServerSession session = new LocalOutgoingServerSession(domain,
connection, new OutgoingServerSocketReader(reader), streamID);
connection.init(session);
// Set the hostname as the address of the session
session.setAddress(new JID(null, hostname, null));
// Set that the session was created using TLS+SASL (no server dialback)
session.usingServerDialback = false;
return session;
}
else {
Log.debug("OS - Error, EXTERNAL SASL authentication with " + hostname +
" failed");
return null;
}
}
}
Log.debug("OS - Error, EXTERNAL SASL was not offered by " + hostname);
}
else {
Log.debug("OS - Error, no SASL mechanisms were offered by " + hostname);
}
}
else {
Log.debug("OS - Error, <proceed> was not received");
}
return null;
}
private static boolean doExternalAuthentication(String domain, SocketConnection connection,
XMPPPacketReader reader) throws DocumentException, IOException, XmlPullParserException {
StringBuilder sb = new StringBuilder();
sb.append("<auth xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\" mechanism=\"EXTERNAL\">");
sb.append(StringUtils.encodeBase64(domain));
sb.append("</auth>");
connection.deliverRawText(sb.toString());
Element response = reader.parseDocument().getRootElement();
return response != null && "success".equals(response.getName());
}
public LocalOutgoingServerSession(String serverName, Connection connection,
OutgoingServerSocketReader socketReader, StreamID streamID) {
super(serverName, connection, streamID);
this.socketReader = socketReader;
socketReader.setSession(this);
}
boolean canProcess(Packet packet) {
String senderDomain = packet.getFrom().getDomain();
if (!getAuthenticatedDomains().contains(senderDomain)) {
synchronized (senderDomain.intern()) {
if (!getAuthenticatedDomains().contains(senderDomain) &&
!authenticateSubdomain(senderDomain, packet.getTo().getDomain())) {
// Return error since sender domain was not validated by remote server
returnErrorToSender(packet);
return false;
}
}
}
return true;
}
void deliver(Packet packet) throws UnauthorizedException {
if (conn != null && !conn.isClosed()) {
conn.deliver(packet);
}
}
public boolean authenticateSubdomain(String domain, String hostname) {
if (!usingServerDialback) {
// Using SASL so just assume that the domain was validated
// (note: this may not be correct)
addAuthenticatedDomain(domain);
return true;
}
ServerDialback method = new ServerDialback(getConnection(), domain);
if (method.authenticateDomain(socketReader, domain, hostname, getStreamID().getID())) {
// Add the validated domain as an authenticated domain
addAuthenticatedDomain(domain);
return true;
}
return false;
}
private void returnErrorToSender(Packet packet) {
RoutingTable routingTable = XMPPServer.getInstance().getRoutingTable();
try {
if (packet instanceof IQ) {
IQ reply = new IQ();
reply.setID(packet.getID());
reply.setTo(packet.getFrom());
reply.setFrom(packet.getTo());
reply.setChildElement(((IQ) packet).getChildElement().createCopy());
reply.setError(PacketError.Condition.remote_server_not_found);
routingTable.routePacket(reply.getTo(), reply);
}
else if (packet instanceof Presence) {
Presence reply = new Presence();
reply.setID(packet.getID());
reply.setTo(packet.getFrom());
reply.setFrom(packet.getTo());
reply.setError(PacketError.Condition.remote_server_not_found);
routingTable.routePacket(reply.getTo(), reply);
}
else if (packet instanceof Message) {
Message reply = new Message();
reply.setID(packet.getID());
reply.setTo(packet.getFrom());
reply.setFrom(packet.getTo());
reply.setType(((Message)packet).getType());
reply.setThread(((Message)packet).getThread());
reply.setError(PacketError.Condition.remote_server_not_found);
routingTable.routePacket(reply.getTo(), reply);
}
}
catch (Exception e) {
Log.warn("Error returning error to sender. Original packet: " + packet, e);
}
}
public Collection<String> getAuthenticatedDomains() {
return Collections.unmodifiableCollection(authenticatedDomains);
}
public void addAuthenticatedDomain(String domain) {
authenticatedDomains.add(domain);
}
public Collection<String> getHostnames() {
return Collections.unmodifiableCollection(hostnames);
}
public void addHostname(String hostname) {
if (hostnames.add(hostname)) {
// Add a new route for this new session
XMPPServer.getInstance().getRoutingTable().addServerRoute(new JID(hostname), this);
}
}
public String getAvailableStreamFeatures() {
// Nothing special to add
return null;
}
public boolean isUsingServerDialback() {
return usingServerDialback;
}
}
\ No newline at end of file
package org.jivesoftware.openfire.session;
import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.interceptor.InterceptorManager;
import org.jivesoftware.openfire.interceptor.PacketRejectedException;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
/**
* The session represents a connection between the server and a client (c2s) or
* another server (s2s) as well as a connection with a component. Authentication and
* user accounts are associated with c2s connections while s2s has an optional authentication
* association but no single user user.<p>
*
* Obtain object managers from the session in order to access server resources.
*
* @author Gaston Dombiak
*/
public abstract class LocalSession implements Session {
/**
* The utf-8 charset for decoding and encoding Jabber packet streams.
*/
protected static String CHARSET = "UTF-8";
/**
* The Address this session is authenticated as.
*/
private JID address;
/**
* The stream id for this session (random and unique).
*/
private StreamID streamID;
/**
* The current session status.
*/
protected int status = STATUS_CONNECTED;
/**
* The connection that this session represents.
*/
protected Connection conn;
protected SessionManager sessionManager;
private String serverName;
private Date startDate = new Date();
private long lastActiveDate;
private long clientPacketCount = 0;
private long serverPacketCount = 0;
/**
* Session temporary data. All data stored in this <code>Map</code> disapear when session
* finishes.
*/
private Map<String, Object> sessionData = null;
/**
* Creates a session with an underlying connection and permission protection.
*
* @param serverName domain of the XMPP server where the new session belongs.
* @param connection The connection we are proxying.
* @param streamID unique identifier for this session.
*/
public LocalSession(String serverName, Connection connection, StreamID streamID) {
conn = connection;
this.streamID = streamID;
this.serverName = serverName;
String id = streamID.getID();
this.address = new JID(null, serverName, id, true);
this.sessionManager = SessionManager.getInstance();
sessionData = new TreeMap<String, Object>();
}
/**
* Obtain the address of the user. The address is used by services like the core
* server packet router to determine if a packet should be sent to the handler.
* Handlers that are working on behalf of the server should use the generic server
* hostname address (e.g. server.com).
*
* @return the address of the packet handler.
*/
public JID getAddress() {
return address;
}
/**
* Sets the new address of this session. The address is used by services like the core
* server packet router to determine if a packet should be sent to the handler.
* Handlers that are working on behalf of the server should use the generic server
* hostname address (e.g. server.com).
*
* @param address the new address of this session.
*/
public void setAddress(JID address){
this.address = address;
}
/**
* Returns the connection associated with this Session.
*
* @return The connection for this session
*/
public Connection getConnection() {
return conn;
}
/**
* Obtain the current status of this session.
*
* @return The status code for this session
*/
public int getStatus() {
return status;
}
/**
* Set the new status of this session. Setting a status may trigger
* certain events to occur (setting a closed status will close this
* session).
*
* @param status The new status code for this session
*/
public void setStatus(int status) {
this.status = status;
}
/**
* Obtain the stream ID associated with this sesison. Stream ID's are generated by the server
* and should be unique and random.
*
* @return This session's assigned stream ID
*/
public StreamID getStreamID() {
return streamID;
}
/**
* Obtain the name of the server this session belongs to.
*
* @return the server name.
*/
public String getServerName() {
return serverName;
}
/**
* Obtain the date the session was created.
*
* @return the session's creation date.
*/
public Date getCreationDate() {
return startDate;
}
/**
* Obtain the time the session last had activity.
*
* @return The last time the session received activity.
*/
public Date getLastActiveDate() {
return new Date(lastActiveDate);
}
/**
* Increments the number of packets sent from the client to the server.
*/
public void incrementClientPacketCount() {
clientPacketCount++;
lastActiveDate = System.currentTimeMillis();
}
/**
* Increments the number of packets sent from the server to the client.
*/
public void incrementServerPacketCount() {
serverPacketCount++;
lastActiveDate = System.currentTimeMillis();
}
/**
* Obtain the number of packets sent from the client to the server.
*
* @return The number of packets sent from the client to the server.
*/
public long getNumClientPackets() {
return clientPacketCount;
}
/**
* Obtain the number of packets sent from the server to the client.
*
* @return The number of packets sent from the server to the client.
*/
public long getNumServerPackets() {
return serverPacketCount;
}
/**
* Saves given session data. Data are saved to temporary storage only and are accessible during
* this session life only and only from this session instance.
*
* @param key a <code>String</code> value of stored data key ID.
* @param value a <code>Object</code> value of data stored in session.
* @see #getSessionData(String)
*/
public void setSessionData(String key, Object value) {
sessionData.put(key, value);
}
/**
* Retrieves session data. This method gives access to temporary session data only. You can
* retrieve earlier saved data giving key ID to receive needed value. Please see
* {@link #setSessionData(String, Object)} description for more details.
*
* @param key a <code>String</code> value of stored data ID.
* @return a <code>Object</code> value of data for given key.
* @see #setSessionData(String, Object)
*/
public Object getSessionData(String key) {
return sessionData.get(key);
}
/**
* Removes session data. Please see {@link #setSessionData(String, Object)} description
* for more details.
*
* @param key a <code>String</code> value of stored data ID.
* @see #setSessionData(String, Object)
*/
public void removeSessionData(String key) {
sessionData.remove(key);
}
public void process(Packet packet) {
// Check that the requested packet can be processed
if (canProcess(packet)) {
// Perform the actual processing of the packet. This usually implies sending
// the packet to the entity
try {
// Invoke the interceptors before we send the packet
InterceptorManager.getInstance().invokeInterceptors(packet, this, false, false);
deliver(packet);
// Invoke the interceptors after we have sent the packet
InterceptorManager.getInstance().invokeInterceptors(packet, this, false, true);
}
catch (PacketRejectedException e) {
// An interceptor rejected the packet so do nothing
}
catch (Exception e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
}
}
/**
* Returns true if the specified packet can be delivered to the entity. Subclasses will use different
* criterias to determine of processing is allowed or not. For instance, client sessions will use
* privacy lists while outgoing server sessions will always allow this action.
*
* @param packet the packet to analyze if it must be blocked.
* @return true if the specified packet must be blocked.
*/
abstract boolean canProcess(Packet packet);
abstract void deliver(Packet packet) throws UnauthorizedException;
public void deliverRawText(String text) {
if (conn != null) {
conn.deliverRawText(text);
}
}
/**
* Returns a text with the available stream features. Each subclass may return different
* values depending whether the session has been authenticated or not.
*
* @return a text with the available stream features or <tt>null</tt> to add nothing.
*/
public abstract String getAvailableStreamFeatures();
public void close() {
if (conn != null) {
conn.close();
}
}
public boolean validate() {
return conn.validate();
}
public boolean isSecure() {
return conn.isSecure();
}
public boolean isClosed() {
return conn.isClosed();
}
public String getHostAddress() throws UnknownHostException {
return conn.getHostAddress();
}
public String getHostName() throws UnknownHostException {
return conn.getHostName();
}
public String toString() {
return super.toString() + " status: " + status + " address: " + address + " id: " + streamID;
}
protected static int[] decodeVersion(String version) {
int[] answer = new int[] {0 , 0};
String [] versionString = version.split("\\.");
answer[0] = Integer.parseInt(versionString[0]);
answer[1] = Integer.parseInt(versionString[1]);
return answer;
}
}
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 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.openfire.session;
import org.xmpp.packet.JID;
/**
* Locator of sessions that are being hosted by other cluster nodes. Use
* {@link org.jivesoftware.openfire.XMPPServer#setRemoteSessionLocator(RemoteSessionLocator)} to
* set the session locator to use. When not running inside of a cluster
* {@link org.jivesoftware.openfire.XMPPServer#getRemoteSessionLocator()} will always return null.
*
* @author Gaston Dombiak
*/
public interface RemoteSessionLocator {
/**
* Returns a session surrogate of a client session hosted by a remote cluster node. It is
* assumed that the client session exists in a remote node. Anyway, if the remote session
* was closed or its connection was lost or if the connection to the cluster is lost then
* any remote invocation will fail and a proper error will be returned.
*
* @param nodeID the ID of the node hosting the session.
* @param address the address that uniquely identifies the session.
* @return a session surrogate of a client session hosted by a remote cluster node.
*/
ClientSession getClientSession(byte[] nodeID, JID address);
/**
* Returns a session surrogate of a component session hosted by a remote cluster node. It is
* assumed that the component session exists in a remote node. Anyway, if the remote session
* was closed or its connection was lost or if the connection to the cluster is lost then
* any remote invocation will fail and a proper error will be returned.
*
* @param nodeID the ID of the node hosting the session.
* @param address the address that uniquely identifies the session.
* @return a session surrogate of a component session hosted by a remote cluster node.
*/
ComponentSession getComponentSession(byte[] nodeID, JID address);
/**
* Returns a session surrogate of a Connection Multiplexer session hosted by a remote cluster node. It is
* assumed that the ConnectionMultiplexer session exists in a remote node. Anyway, if the remote session
* was closed or its connection was lost or if the connection to the cluster is lost then
* any remote invocation will fail and a proper error will be returned.
*
* @param nodeID the ID of the node hosting the session.
* @param address the address that uniquely identifies the session.
* @return a session surrogate of a ConnectionMultiplexer session hosted by a remote cluster node.
*/
ConnectionMultiplexerSession getConnectionMultiplexerSession(byte[] nodeID, JID address);
/**
* Returns a session surrogate of an incoming server session hosted by a remote cluster node. It is
* assumed that the incoming server session exists in a remote node. Anyway, if the remote session
* was closed or its connection was lost or if the connection to the cluster is lost then
* any remote invocation will fail and a proper error will be returned.
*
* @param nodeID the ID of the node hosting the session.
* @param streamID the stream ID that uniquely identifies the session.
* @return a session surrogate of an incoming server session hosted by a remote cluster node.
*/
IncomingServerSession getIncomingServerSession(byte[] nodeID, String streamID);
/**
* Returns a session surrogate of an outgoing server session hosted by a remote cluster node. It is
* assumed that the outgoing server session exists in a remote node. Anyway, if the remote session
* was closed or its connection was lost or if the connection to the cluster is lost then
* any remote invocation will fail and a proper error will be returned.
*
* @param nodeID the ID of the node hosting the session.
* @param address the address that uniquely identifies the session.
* @return a session surrogate of an incoming server session hosted by a remote cluster node.
*/
OutgoingServerSession getOutgoingServerSession(byte[] nodeID, JID address);
}
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