
rocks.xmpp.core.session.XmppClient Maven / Gradle / Ivy
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.core.session;
import rocks.xmpp.addr.Jid;
import rocks.xmpp.core.XmppException;
import rocks.xmpp.core.bind.model.Bind;
import rocks.xmpp.core.sasl.AuthenticationException;
import rocks.xmpp.core.sasl.model.Mechanisms;
import rocks.xmpp.core.session.model.Session;
import rocks.xmpp.core.stanza.StanzaException;
import rocks.xmpp.core.stanza.model.IQ;
import rocks.xmpp.core.stanza.model.Message;
import rocks.xmpp.core.stanza.model.Presence;
import rocks.xmpp.core.stanza.model.client.ClientIQ;
import rocks.xmpp.core.stanza.model.client.ClientMessage;
import rocks.xmpp.core.stanza.model.client.ClientPresence;
import rocks.xmpp.core.stream.StreamErrorException;
import rocks.xmpp.core.stream.StreamFeatureNegotiator;
import rocks.xmpp.core.stream.StreamNegotiationException;
import rocks.xmpp.core.stream.model.StreamElement;
import rocks.xmpp.extensions.caps.EntityCapabilitiesManager;
import rocks.xmpp.extensions.sm.StreamManager;
import rocks.xmpp.im.roster.RosterManager;
import rocks.xmpp.im.subscription.PresenceManager;
import rocks.xmpp.util.concurrent.AsyncResult;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.sasl.RealmCallback;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The base class for establishing an XMPP session with a server, i.e. client-to-server sessions.
* Establishing an XMPP Session
* The following example shows the most simple way to establish a session:
*
* {@code
* XmppClient xmppClient = XmppClient.create("domain");
* xmppClient.connect();
* xmppClient.login("username", "password");
* }
*
* By default, the session will try to establish a TCP connection over port 5222 and will try BOSH as fallback.
* You can configure a session and its connection methods by passing appropriate configurations in its constructor.
* Sending Messages
* Once connected, you can send messages:
*
* {@code
* xmppClient.sendMessage(new Message(Jid.of("[email protected]"), Message.Type.CHAT));
* }
*
* Closing the Session
*
* {@code
* xmppClient.close();
* }
*
* Listening for Messages and Presence
* Note: Adding the following listeners should be added before logging in, otherwise they might not trigger.
*
* {@code
* // Listen for messages
* xmppClient.addInboundMessageListener(e ->
* // Handle inbound message.
* );
*
* // Listen for presence changes
* xmppClient.addInboundPresenceListener(e ->
* // Handle inbound presence.
* );
* }
*
* This class is thread-safe, which means you can safely add listeners or call send()
, close()
(and other methods) from different threads.
*
* @author Christian Schudt
* @see XmppSessionConfiguration
* @see TcpConnectionConfiguration
* @see rocks.xmpp.extensions.httpbind.BoshConnectionConfiguration
*/
public final class XmppClient extends XmppSession {
private static final Logger logger = Logger.getLogger(XmppClient.class.getName());
private final AuthenticationManager authenticationManager;
/**
* The user, which is assigned by the server after resource binding.
*/
private volatile Jid connectedResource;
/**
* The resource, which the user requested during resource binding. This value is stored, so that it can be reused during reconnection.
*/
private volatile String resource;
private volatile String lastAuthorizationId;
private volatile Collection lastMechanisms;
private volatile CallbackHandler lastCallbackHandler;
private volatile boolean anonymous;
/**
* Creates a session with the specified service domain, by using the default configuration.
*
* @param xmppServiceDomain The service domain.
* @param connectionConfigurations The connection configurations.
* @deprecated Use {@link #create(String, ConnectionConfiguration...)}
*/
@Deprecated
public XmppClient(String xmppServiceDomain, ConnectionConfiguration... connectionConfigurations) {
this(xmppServiceDomain, XmppSessionConfiguration.getDefault(), connectionConfigurations);
}
/**
* Creates a session with the specified service domain by using a configuration.
*
* @param xmppServiceDomain The service domain.
* @param configuration The configuration.
* @param connectionConfigurations The connection configurations.
* @deprecated Use {@link #create(String, XmppSessionConfiguration, ConnectionConfiguration...)}
*/
@Deprecated
public XmppClient(String xmppServiceDomain, XmppSessionConfiguration configuration, ConnectionConfiguration... connectionConfigurations) {
super(xmppServiceDomain, configuration, connectionConfigurations);
authenticationManager = new AuthenticationManager(this);
streamFeaturesManager.addFeatureNegotiator(authenticationManager);
streamFeaturesManager.addFeatureNegotiator(new StreamFeatureNegotiator(this, Bind.class) {
@Override
public Status processNegotiation(Object element) throws StreamNegotiationException {
// Resource binding will be negotiated manually
return Status.INCOMPLETE;
}
@Override
public boolean canProcess(Object element) {
return false;
}
});
}
/**
* Creates a new XMPP client instance. Any registered {@link #addCreationListener(Consumer) creation listeners} are triggered.
*
* @param xmppServiceDomain The XMPP service domain.
* @param connectionConfigurations The connection methods, which are used to connect.
* @return The XMPP client.
*/
public static XmppClient create(String xmppServiceDomain, ConnectionConfiguration... connectionConfigurations) {
return create(xmppServiceDomain, XmppSessionConfiguration.getDefault(), connectionConfigurations);
}
/**
* Creates a new XMPP client instance. Any registered {@link #addCreationListener(Consumer) creation listeners} are triggered.
*
* @param xmppServiceDomain The XMPP service domain.
* @param configuration The configuration.
* @param connectionConfigurations The connection methods, which are used to connect.
* @return The XMPP client.
*/
public static XmppClient create(String xmppServiceDomain, XmppSessionConfiguration configuration, ConnectionConfiguration... connectionConfigurations) {
XmppClient xmppClient = new XmppClient(xmppServiceDomain, configuration, connectionConfigurations);
notifyCreationListeners(xmppClient);
return xmppClient;
}
/**
* Connects to the XMPP server.
*
* @param from The 'from' attribute.
* @throws ConnectionException If a connection error occurred on the transport layer, e.g. the socket could not connect.
* @throws StreamErrorException If the server returned a stream error.
* @throws StreamNegotiationException If any exception occurred during stream feature negotiation.
* @throws NoResponseException If the server didn't return a response during stream establishment.
* @throws XmppException If any other XMPP exception occurs.
* @throws IllegalStateException If the session is in a wrong state, e.g. closed or already connected.
*/
@Override
public final void connect(Jid from) throws XmppException {
Status previousStatus = preConnect();
if (checkConnected()) {
// Silently return, when we are already connected or connecting.
return;
}
try {
// Don't call listeners from within synchronized blocks to avoid possible deadlocks.
updateStatus(Status.CONNECTING);
synchronized (this) {
// Double-checked locking: Recheck connected status. In a multi-threaded environment multiple threads could have passed the first check.
if (checkConnected()) {
return;
}
// Reset
exception = null;
tryConnect(from, "jabber:client", this::setXmppServiceDomain);
logger.fine("Negotiating stream, waiting until SASL is ready to be negotiated.");
// Check if connecting failed with an exception.
throwAsXmppExceptionIfNotNull(exception);
// Wait until the reader thread signals, that we are connected. That is after TLS negotiation and before SASL negotiation.
try {
streamFeaturesManager.awaitNegotiation(Mechanisms.class).get(configuration.getDefaultResponseTimeout().toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new NoResponseException("Timeout while waiting on advertised authentication mechanisms.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw e;
} catch (CancellationException e) {
throwAsXmppExceptionIfNotNull(exception != null ? exception : e);
}
// Check if stream negotiation threw any exception.
throwAsXmppExceptionIfNotNull(exception);
logger.fine("Stream negotiated until SASL, now ready to login.");
}
// If a secure connection has been configured, but hasn't been negotiated for some reason (e.g. MitM attack), throw an exception.
if (!activeConnection.isSecure() && activeConnection.getConfiguration().isSecure()) {
throw new StreamNegotiationException("Transport Layer Security has been configured, but hasn't been negotiated.");
}
// Don't call listeners from within synchronized blocks to avoid possible deadlocks.
updateStatus(Status.CONNECTING, Status.CONNECTED);
// This is for reconnection.
if (wasLoggedIn) {
logger.fine("Was already logged in. Re-login automatically with known credentials.");
login(lastMechanisms, lastAuthorizationId, lastCallbackHandler, resource);
}
} catch (Throwable e) {
onConnectionFailed(previousStatus, e);
}
}
/**
* Authenticates against the server and binds a random resource (assigned by the server).
*
* @param user The user name. Usually this is the local part of the user's JID. Must not be null.
* @param password The password. Must not be null.
* @return The additional data with success, i.e. the data returned upon successful authentication.
* @throws AuthenticationException If the login failed, due to a SASL error reported by the server.
* @throws StreamErrorException If the server returned a stream error.
* @throws StreamNegotiationException If any exception occurred during stream feature negotiation.
* @throws NoResponseException If the server didn't return a response during stream establishment.
* @throws StanzaException If the server returned a stanza error during resource binding or roster retrieval.
* @throws XmppException If the login failed, due to another error.
*/
public final byte[] login(String user, String password) throws XmppException {
return login(user, password, null);
}
/**
* Authenticates against the server with username/password credential and binds a resource.
*
* @param user The user name. Usually this is the local part of the user's JID. Must not be null.
* @param password The password. Must not be null.
* @param resource The resource. If null or empty, the resource is randomly assigned by the server.
* @return The additional data with success, i.e. the data returned upon successful authentication.
* @throws AuthenticationException If the login failed, due to a SASL error reported by the server.
* @throws StreamErrorException If the server returned a stream error.
* @throws StreamNegotiationException If any exception occurred during stream feature negotiation.
* @throws NoResponseException If the server didn't return a response during stream establishment.
* @throws StanzaException If the server returned a stanza error during resource binding or roster retrieval.
* @throws XmppException If the login failed, due to another error.
*/
public final byte[] login(String user, String password, String resource) throws XmppException {
return login(null, user, password, resource);
}
/**
* Authenticates against the server with an authorization id and username/password credential and binds a resource.
*
* @param authorizationId The authorization id.
* @param user The user name. Usually this is the local part of the user's JID. Must not be null.
* @param password The password. Must not be null.
* @param resource The resource. If null or empty, the resource is randomly assigned by the server.
* @return The additional data with success, i.e. the data returned upon successful authentication.
* @throws AuthenticationException If the login failed, due to a SASL error reported by the server.
* @throws StreamErrorException If the server returned a stream error.
* @throws StreamNegotiationException If any exception occurred during stream feature negotiation.
* @throws NoResponseException If the server didn't return a response during stream establishment.
* @throws StanzaException If the server returned a stanza error during resource binding or roster retrieval.
* @throws XmppException If the login failed, due to another error.
*/
public final byte[] login(String authorizationId, final String user, final String password, String resource) throws XmppException {
Objects.requireNonNull(user, "user must not be null.");
Objects.requireNonNull(password, "password must not be null.");
// A default callback handler for username/password retrieval:
return login(authorizationId, callbacks -> Arrays.stream(callbacks).forEach(callback -> {
if (callback instanceof NameCallback) {
((NameCallback) callback).setName(user);
}
if (callback instanceof PasswordCallback) {
((PasswordCallback) callback).setPassword(password.toCharArray());
}
if (callback instanceof RealmCallback) {
((RealmCallback) callback).setText(((RealmCallback) callback).getDefaultText());
}
}), resource);
}
/**
* Authenticates against the server with a custom callback handler and binds a resource.
*
* @param authorizationId The authorization id.
* @param callbackHandler The callback handler.
* @param resource The resource. If null or empty, the resource is randomly assigned by the server.
* @return The additional data with success, i.e. the data returned upon successful authentication.
* @throws AuthenticationException If the login failed, due to a SASL error reported by the server.
* @throws StreamErrorException If the server returned a stream error.
* @throws StreamNegotiationException If any exception occurred during stream feature negotiation.
* @throws NoResponseException If the server didn't return a response during stream establishment.
* @throws StanzaException If the server returned a stanza error during resource binding or roster retrieval.
* @throws XmppException If the login failed, due to another error.
*/
public final byte[] login(String authorizationId, CallbackHandler callbackHandler, String resource) throws XmppException {
return login(configuration.getAuthenticationMechanisms(), authorizationId, callbackHandler, resource);
}
/**
* Logs in anonymously and binds a resource.
*
* @return The additional data with success, i.e. the data returned upon successful authentication.
* @throws AuthenticationException If the login failed, due to a SASL error reported by the server.
* @throws StreamErrorException If the server returned a stream error.
* @throws StreamNegotiationException If any exception occurred during stream feature negotiation.
* @throws NoResponseException If the server didn't return a response during stream establishment.
* @throws StanzaException If the server returned a stanza error during resource binding.
* @throws XmppException If the login failed, due to another error.
*/
public final byte[] loginAnonymously() throws XmppException {
byte[] successData = login(Collections.singleton("ANONYMOUS"), null, null, null);
anonymous = true;
return successData;
}
private byte[] login(Collection mechanisms, String authorizationId, CallbackHandler callbackHandler, String resource) throws XmppException {
if (checkAuthenticated()) {
// Silently return, when we are already authenticated.
return authenticationManager.getSuccessData();
}
Status previousStatus = preLogin();
updateStatus(Status.AUTHENTICATING);
synchronized (this) {
if (checkAuthenticated()) {
// Silently return, when we are already authenticated.
return authenticationManager.getSuccessData();
}
lastMechanisms = mechanisms;
lastAuthorizationId = authorizationId;
lastCallbackHandler = callbackHandler;
try {
long timeout = configuration.getDefaultResponseTimeout().toMillis();
logger.fine("Starting SASL negotiation (authentication).");
if (callbackHandler == null) {
authenticationManager.startAuthentication(mechanisms, null, null);
} else {
authenticationManager.startAuthentication(mechanisms, authorizationId, callbackHandler);
}
// Negotiate all pending features until would be negotiated.
try {
streamFeaturesManager.awaitNegotiation(Bind.class).get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new NoResponseException("Timeout while waiting on resource binding feature.");
}
// Check if stream feature negotiation failed with an exception.
throwAsXmppExceptionIfNotNull(exception);
// Stream resumption.
try {
StreamManager streamManager = getManager(StreamManager.class);
if (streamManager.resume().getResult(timeout, TimeUnit.MILLISECONDS)) {
logger.fine("Stream resumed.");
updateStatus(Status.AUTHENTICATED);
afterLogin();
return authenticationManager.getSuccessData();
}
} catch (TimeoutException e) {
logger.warning("Could not resume stream due to timeout.");
}
// Then negotiate resource binding manually.
bindResource(resource);
// Proceed with any outstanding stream features which are negotiated after resource binding, e.g. XEP-0198
// and wait until all features have been negotiated.
try {
streamFeaturesManager.completeNegotiation().get(timeout * 2, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new NoResponseException("Timeout while waiting on stream feature negotiation to finish.");
}
// Check again, if stream feature negotiation failed with an exception.
throwAsXmppExceptionIfNotNull(exception);
logger.fine("Stream negotiation completed successfully.");
// Retrieve roster.
RosterManager rosterManager = getManager(RosterManager.class);
if (callbackHandler != null && rosterManager.isEnabled() && rosterManager.isRetrieveRosterOnLogin()) {
logger.fine("Retrieving roster on login (as per configuration).");
try {
rosterManager.requestRoster().getResult(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.warning("Could not retrieve roster in time.");
}
}
PresenceManager presenceManager = getManager(PresenceManager.class);
if (presenceManager.getLastSentPresence() != null) {
// After retrieving the roster, resend the last presence, if any (in reconnection case).
// Note, that this will also rejoin any Multi-User Chats on reconnection.
// It's important to first rejoin them, before resending unacknowledged MUC messages.
presenceManager.getLastSentPresences().forEach(presence -> {
presence.getExtensions().clear();
send(presence, false);
});
} else if (configuration.getInitialPresence() != null) {
// Or send initial presence
Presence initialPresence = configuration.getInitialPresence().get();
if (initialPresence != null) {
send(initialPresence, false);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// Revert status
updateStatus(previousStatus, e);
throwAsXmppExceptionIfNotNull(e);
} catch (CancellationException e) {
Throwable cause = exception != null ? exception : e;
// Revert status
updateStatus(previousStatus, cause);
throwAsXmppExceptionIfNotNull(cause);
} catch (Throwable e) {
// Revert status
updateStatus(previousStatus, e);
throwAsXmppExceptionIfNotNull(e);
}
logger.fine("Login successful.");
afterLogin();
return authenticationManager.getSuccessData();
}
}
/**
* Binds a resource to the session.
*
* @param resource The resource to bind. If the resource is null and random resource is bound by the server.
*/
private void bindResource(String resource) throws XmppException {
this.resource = resource;
logger.log(Level.FINE, "Negotiating resource binding, resource: {0}.", resource);
// Bind the resource
IQ result;
try {
result = query(IQ.set(new Bind(this.resource))).getResult(configuration.getDefaultResponseTimeout().toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw new NoResponseException("Could not bind resource due to timeout.");
}
Bind bindResult = result.getExtension(Bind.class);
this.connectedResource = bindResult.getJid();
logger.log(Level.FINE, "Resource binding completed, connected resource: {0}.", connectedResource);
// At this point the entity is free to send stanzas:
// "If, before completing the resource binding step, the client attempts to send an XML stanza to an entity other
// than the server itself or the client's account, the server MUST NOT process the stanza
// and MUST close the stream with a stream error."
// Deprecated method of session binding, according to the old specification
// This is no longer used, according to the updated specification.
// But some old server implementation still require it.
Session session = (Session) streamFeaturesManager.getFeatures().get(Session.class);
if (session != null && session.isMandatory()) {
logger.fine("Establishing session.");
query(IQ.set(new Session()));
}
// Set this status after session establishment. It's used to auto-send service discovery to a server and some servers won't response,
// if it's send before.
updateStatus(Status.AUTHENTICATED);
}
/**
* Gets the connected resource, which is assigned by the server after resource binding.
*
*
* After a client has bound a resource to the stream, it is referred to as a "connected resource".
*
*
* @return The connected resource.
*/
public final Jid getConnectedResource() {
return connectedResource;
}
/**
* Indicates whether the session has been logged in anonymously. If never logged in at all, returns false.
*
* @return True, if the session is anonymous.
*/
public final boolean isAnonymous() {
return anonymous;
}
@Override
protected final StreamElement prepareElement(StreamElement element) {
if (element instanceof Message) {
element = ClientMessage.from((Message) element);
} else if (element instanceof Presence) {
element = ClientPresence.from((Presence) element);
} else if (element instanceof IQ) {
element = ClientIQ.from((IQ) element);
}
return element;
}
/**
* Determines support of another XMPP entity for a given feature.
*
* Note that if you want to determine support of another client, you have to provide that client's full JID (user@domain/resource).
* If you want to determine the server's capabilities provide only the domain JID of the server.
*
* This method uses cached information and the presence based entity capabilities (XEP-0115) to determine support. Only if no information is available an explicit service discovery request is made.
*
* @param feature The feature, usually defined by an XMPP Extension Protocol, e.g. "urn:xmpp:ping".
* @param jid The XMPP entity.
* @return True, if the XMPP entity supports the given feature; otherwise false.
*/
public final AsyncResult isSupported(String feature, Jid jid) {
return getManager(EntityCapabilitiesManager.class).isSupported(feature, jid);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy