mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-27 06:22:07 +01:00
b16f34f61e
Smack's Managers should not remove itself when the connection is closed or should re-add themselves if the connection get reconnected. This should also fix some NPE's. We are currently going with two different designs of Manager: 1. The one with WeakReferences/WeakHashMaps (SDM, EntityCapsManager) and 2. the one where the managers remove their listeners on connectionClosed() *and* connectionClosedOnError(), and later add their listeners on reconnectionSuccessful(). The first design has the Connection instance only weak referenced. The other design does reference Connection strongly (e.g. the 'managers' map in IBBManager/S5BManager), but removes this references when connectionClosed(onError)() is called. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_2@13788 b35dd754-fafc-0310-a699-88a17e54d16e
558 lines
22 KiB
Java
558 lines
22 KiB
Java
/**
|
|
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package org.jivesoftware.smackx.bytestreams.ibb;
|
|
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Random;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
import org.jivesoftware.smack.AbstractConnectionListener;
|
|
import org.jivesoftware.smack.Connection;
|
|
import org.jivesoftware.smack.ConnectionCreationListener;
|
|
import org.jivesoftware.smack.XMPPException;
|
|
import org.jivesoftware.smack.packet.IQ;
|
|
import org.jivesoftware.smack.packet.XMPPError;
|
|
import org.jivesoftware.smack.util.SyncPacketSend;
|
|
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
|
|
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
|
|
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
|
|
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
|
|
|
|
/**
|
|
* The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
|
|
* href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
|
|
* <p>
|
|
* The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
|
|
* they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
|
|
* in case the Socks5 bytestream method of transferring data is not available.
|
|
* <p>
|
|
* There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
|
|
* send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by
|
|
* the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
|
|
* stanzas are not acknowledged because most XMPP server implementation don't support stanza
|
|
* flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
|
|
* Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
|
|
* <p>
|
|
* To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will
|
|
* negotiate an in-band bytestream with the given target JID and return a session.
|
|
* <p>
|
|
* If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
|
|
* transfer) invoke {@link #establishSession(String, String)}.
|
|
* <p>
|
|
* To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
|
|
* manager. There are two ways to add this listener. If you want to be informed about incoming
|
|
* In-Band Bytestreams from a specific user add the listener by invoking
|
|
* {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
|
|
* respond to all In-Band Bytestream requests invoke
|
|
* {@link #addIncomingBytestreamListener(BytestreamListener)}.
|
|
* <p>
|
|
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
|
* In-Band bytestream requests sent in the context of <a
|
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
|
* {@link FileTransferManager})
|
|
* <p>
|
|
* If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
|
|
* will be rejected by returning a <not-acceptable/> error to the initiator.
|
|
*
|
|
* @author Henning Staib
|
|
*/
|
|
public class InBandBytestreamManager implements BytestreamManager {
|
|
|
|
/**
|
|
* Stanzas that can be used to encapsulate In-Band Bytestream data packets.
|
|
*/
|
|
public enum StanzaType {
|
|
|
|
/**
|
|
* IQ stanza.
|
|
*/
|
|
IQ,
|
|
|
|
/**
|
|
* Message stanza.
|
|
*/
|
|
MESSAGE
|
|
}
|
|
|
|
/*
|
|
* create a new InBandBytestreamManager and register its shutdown listener on every established
|
|
* connection
|
|
*/
|
|
static {
|
|
Connection.addConnectionCreationListener(new ConnectionCreationListener() {
|
|
public void connectionCreated(final Connection connection) {
|
|
// create the manager for this connection
|
|
InBandBytestreamManager.getByteStreamManager(connection);
|
|
|
|
// register shutdown listener
|
|
connection.addConnectionListener(new AbstractConnectionListener() {
|
|
|
|
@Override
|
|
public void connectionClosed() {
|
|
InBandBytestreamManager.getByteStreamManager(connection).disableService();
|
|
}
|
|
|
|
@Override
|
|
public void connectionClosedOnError(Exception e) {
|
|
InBandBytestreamManager.getByteStreamManager(connection).disableService();
|
|
}
|
|
|
|
@Override
|
|
public void reconnectionSuccessful() {
|
|
// re-create the manager for this connection
|
|
InBandBytestreamManager.getByteStreamManager(connection);
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The XMPP namespace of the In-Band Bytestream
|
|
*/
|
|
public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
|
|
|
|
/**
|
|
* Maximum block size that is allowed for In-Band Bytestreams
|
|
*/
|
|
public static final int MAXIMUM_BLOCK_SIZE = 65535;
|
|
|
|
/* prefix used to generate session IDs */
|
|
private static final String SESSION_ID_PREFIX = "jibb_";
|
|
|
|
/* random generator to create session IDs */
|
|
private final static Random randomGenerator = new Random();
|
|
|
|
/* stores one InBandBytestreamManager for each XMPP connection */
|
|
private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();
|
|
|
|
/* XMPP connection */
|
|
private final Connection connection;
|
|
|
|
/*
|
|
* assigns a user to a listener that is informed if an In-Band Bytestream request for this user
|
|
* is received
|
|
*/
|
|
private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
|
|
|
|
/*
|
|
* list of listeners that respond to all In-Band Bytestream requests if there are no user
|
|
* specific listeners for that request
|
|
*/
|
|
private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
|
|
|
|
/* listener that handles all incoming In-Band Bytestream requests */
|
|
private final InitiationListener initiationListener;
|
|
|
|
/* listener that handles all incoming In-Band Bytestream IQ data packets */
|
|
private final DataListener dataListener;
|
|
|
|
/* listener that handles all incoming In-Band Bytestream close requests */
|
|
private final CloseListener closeListener;
|
|
|
|
/* assigns a session ID to the In-Band Bytestream session */
|
|
private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
|
|
|
|
/* block size used for new In-Band Bytestreams */
|
|
private int defaultBlockSize = 4096;
|
|
|
|
/* maximum block size allowed for this connection */
|
|
private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
|
|
|
|
/* the stanza used to send data packets */
|
|
private StanzaType stanza = StanzaType.IQ;
|
|
|
|
/*
|
|
* list containing session IDs of In-Band Bytestream open packets that should be ignored by the
|
|
* InitiationListener
|
|
*/
|
|
private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
|
|
|
|
/**
|
|
* Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
|
|
* {@link Connection}.
|
|
*
|
|
* @param connection the XMPP connection
|
|
* @return the InBandBytestreamManager for the given XMPP connection
|
|
*/
|
|
public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {
|
|
if (connection == null)
|
|
return null;
|
|
InBandBytestreamManager manager = managers.get(connection);
|
|
if (manager == null) {
|
|
manager = new InBandBytestreamManager(connection);
|
|
managers.put(connection, manager);
|
|
}
|
|
return manager;
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param connection the XMPP connection
|
|
*/
|
|
private InBandBytestreamManager(Connection connection) {
|
|
this.connection = connection;
|
|
|
|
// register bytestream open packet listener
|
|
this.initiationListener = new InitiationListener(this);
|
|
this.connection.addPacketListener(this.initiationListener,
|
|
this.initiationListener.getFilter());
|
|
|
|
// register bytestream data packet listener
|
|
this.dataListener = new DataListener(this);
|
|
this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());
|
|
|
|
// register bytestream close packet listener
|
|
this.closeListener = new CloseListener(this);
|
|
this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());
|
|
|
|
}
|
|
|
|
/**
|
|
* Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
|
|
* unless there is a user specific InBandBytestreamListener registered.
|
|
* <p>
|
|
* If no listeners are registered all In-Band Bytestream request are rejected with a
|
|
* <not-acceptable/> error.
|
|
* <p>
|
|
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
|
* Socks5 bytestream requests sent in the context of <a
|
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
|
* {@link FileTransferManager})
|
|
*
|
|
* @param listener the listener to register
|
|
*/
|
|
public void addIncomingBytestreamListener(BytestreamListener listener) {
|
|
this.allRequestListeners.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Removes the given listener from the list of listeners for all incoming In-Band Bytestream
|
|
* requests.
|
|
*
|
|
* @param listener the listener to remove
|
|
*/
|
|
public void removeIncomingBytestreamListener(BytestreamListener listener) {
|
|
this.allRequestListeners.remove(listener);
|
|
}
|
|
|
|
/**
|
|
* Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
|
|
* from the given user.
|
|
* <p>
|
|
* Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
|
|
* user.
|
|
* <p>
|
|
* If no listeners are registered all In-Band Bytestream request are rejected with a
|
|
* <not-acceptable/> error.
|
|
* <p>
|
|
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
|
|
* Socks5 bytestream requests sent in the context of <a
|
|
* href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
|
|
* {@link FileTransferManager})
|
|
*
|
|
* @param listener the listener to register
|
|
* @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
|
|
*/
|
|
public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
|
|
this.userListeners.put(initiatorJID, listener);
|
|
}
|
|
|
|
/**
|
|
* Removes the listener for the given user.
|
|
*
|
|
* @param initiatorJID the JID of the user the listener should be removed
|
|
*/
|
|
public void removeIncomingBytestreamListener(String initiatorJID) {
|
|
this.userListeners.remove(initiatorJID);
|
|
}
|
|
|
|
/**
|
|
* Use this method to ignore the next incoming In-Band Bytestream request containing the given
|
|
* session ID. No listeners will be notified for this request and and no error will be returned
|
|
* to the initiator.
|
|
* <p>
|
|
* This method should be used if you are awaiting an In-Band Bytestream request as a reply to
|
|
* another packet (e.g. file transfer).
|
|
*
|
|
* @param sessionID to be ignored
|
|
*/
|
|
public void ignoreBytestreamRequestOnce(String sessionID) {
|
|
this.ignoredBytestreamRequests.add(sessionID);
|
|
}
|
|
|
|
/**
|
|
* Returns the default block size that is used for all outgoing in-band bytestreams for this
|
|
* connection.
|
|
* <p>
|
|
* The recommended default block size is 4096 bytes. See <a
|
|
* href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
|
|
*
|
|
* @return the default block size
|
|
*/
|
|
public int getDefaultBlockSize() {
|
|
return defaultBlockSize;
|
|
}
|
|
|
|
/**
|
|
* Sets the default block size that is used for all outgoing in-band bytestreams for this
|
|
* connection.
|
|
* <p>
|
|
* The default block size must be between 1 and 65535 bytes. The recommended default block size
|
|
* is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
|
|
* Section 5.
|
|
*
|
|
* @param defaultBlockSize the default block size to set
|
|
*/
|
|
public void setDefaultBlockSize(int defaultBlockSize) {
|
|
if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
|
|
throw new IllegalArgumentException("Default block size must be between 1 and "
|
|
+ MAXIMUM_BLOCK_SIZE);
|
|
}
|
|
this.defaultBlockSize = defaultBlockSize;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
|
|
* <p>
|
|
* Incoming In-Band Bytestream open request will be rejected with an
|
|
* <resource-constraint/> error if the block size is greater then the maximum allowed
|
|
* block size.
|
|
* <p>
|
|
* The default maximum block size is 65535 bytes.
|
|
*
|
|
* @return the maximum block size
|
|
*/
|
|
public int getMaximumBlockSize() {
|
|
return maximumBlockSize;
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
|
|
* <p>
|
|
* The maximum block size must be between 1 and 65535 bytes.
|
|
* <p>
|
|
* Incoming In-Band Bytestream open request will be rejected with an
|
|
* <resource-constraint/> error if the block size is greater then the maximum allowed
|
|
* block size.
|
|
*
|
|
* @param maximumBlockSize the maximum block size to set
|
|
*/
|
|
public void setMaximumBlockSize(int maximumBlockSize) {
|
|
if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
|
|
throw new IllegalArgumentException("Maximum block size must be between 1 and "
|
|
+ MAXIMUM_BLOCK_SIZE);
|
|
}
|
|
this.maximumBlockSize = maximumBlockSize;
|
|
}
|
|
|
|
/**
|
|
* Returns the stanza used to send data packets.
|
|
* <p>
|
|
* Default is {@link StanzaType#IQ}. See <a
|
|
* href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
|
|
*
|
|
* @return the stanza used to send data packets
|
|
*/
|
|
public StanzaType getStanza() {
|
|
return stanza;
|
|
}
|
|
|
|
/**
|
|
* Sets the stanza used to send data packets.
|
|
* <p>
|
|
* The use of {@link StanzaType#IQ} is recommended. See <a
|
|
* href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
|
|
*
|
|
* @param stanza the stanza to set
|
|
*/
|
|
public void setStanza(StanzaType stanza) {
|
|
this.stanza = stanza;
|
|
}
|
|
|
|
/**
|
|
* Establishes an In-Band Bytestream with the given user and returns the session to send/receive
|
|
* data to/from the user.
|
|
* <p>
|
|
* Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
|
|
* Bytestream requests since this method doesn't provide a way to tell the user something about
|
|
* the data to be sent.
|
|
* <p>
|
|
* To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
|
|
* transfer) use {@link #establishSession(String, String)}.
|
|
*
|
|
* @param targetJID the JID of the user an In-Band Bytestream should be established
|
|
* @return the session to send/receive data to/from the user
|
|
* @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
|
|
* user prefers smaller block sizes
|
|
*/
|
|
public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {
|
|
String sessionID = getNextSessionID();
|
|
return establishSession(targetJID, sessionID);
|
|
}
|
|
|
|
/**
|
|
* Establishes an In-Band Bytestream with the given user using the given session ID and returns
|
|
* the session to send/receive data to/from the user.
|
|
*
|
|
* @param targetJID the JID of the user an In-Band Bytestream should be established
|
|
* @param sessionID the session ID for the In-Band Bytestream request
|
|
* @return the session to send/receive data to/from the user
|
|
* @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
|
|
* user prefers smaller block sizes
|
|
*/
|
|
public InBandBytestreamSession establishSession(String targetJID, String sessionID)
|
|
throws XMPPException {
|
|
Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
|
|
byteStreamRequest.setTo(targetJID);
|
|
|
|
// sending packet will throw exception on timeout or error reply
|
|
SyncPacketSend.getReply(this.connection, byteStreamRequest);
|
|
|
|
InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
|
|
this.connection, byteStreamRequest, targetJID);
|
|
this.sessions.put(sessionID, inBandBytestreamSession);
|
|
|
|
return inBandBytestreamSession;
|
|
}
|
|
|
|
/**
|
|
* Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
|
|
* not accepted.
|
|
*
|
|
* @param request IQ packet that should be answered with a not-acceptable error
|
|
*/
|
|
protected void replyRejectPacket(IQ request) {
|
|
XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
|
|
IQ error = IQ.createErrorResponse(request, xmppError);
|
|
this.connection.sendPacket(error);
|
|
}
|
|
|
|
/**
|
|
* Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
|
|
* request is rejected because its block size is greater than the maximum allowed block size.
|
|
*
|
|
* @param request IQ packet that should be answered with a resource-constraint error
|
|
*/
|
|
protected void replyResourceConstraintPacket(IQ request) {
|
|
XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
|
|
IQ error = IQ.createErrorResponse(request, xmppError);
|
|
this.connection.sendPacket(error);
|
|
}
|
|
|
|
/**
|
|
* Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
|
|
* session could not be found.
|
|
*
|
|
* @param request IQ packet that should be answered with a item-not-found error
|
|
*/
|
|
protected void replyItemNotFoundPacket(IQ request) {
|
|
XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
|
|
IQ error = IQ.createErrorResponse(request, xmppError);
|
|
this.connection.sendPacket(error);
|
|
}
|
|
|
|
/**
|
|
* Returns a new unique session ID.
|
|
*
|
|
* @return a new unique session ID
|
|
*/
|
|
private String getNextSessionID() {
|
|
StringBuilder buffer = new StringBuilder();
|
|
buffer.append(SESSION_ID_PREFIX);
|
|
buffer.append(Math.abs(randomGenerator.nextLong()));
|
|
return buffer.toString();
|
|
}
|
|
|
|
/**
|
|
* Returns the XMPP connection.
|
|
*
|
|
* @return the XMPP connection
|
|
*/
|
|
protected Connection getConnection() {
|
|
return this.connection;
|
|
}
|
|
|
|
/**
|
|
* Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
|
|
* request from the given initiator JID is received.
|
|
*
|
|
* @param initiator the initiator's JID
|
|
* @return the listener
|
|
*/
|
|
protected BytestreamListener getUserListener(String initiator) {
|
|
return this.userListeners.get(initiator);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of {@link InBandBytestreamListener} that are informed if there are no
|
|
* listeners for a specific initiator.
|
|
*
|
|
* @return list of listeners
|
|
*/
|
|
protected List<BytestreamListener> getAllRequestListeners() {
|
|
return this.allRequestListeners;
|
|
}
|
|
|
|
/**
|
|
* Returns the sessions map.
|
|
*
|
|
* @return the sessions map
|
|
*/
|
|
protected Map<String, InBandBytestreamSession> getSessions() {
|
|
return sessions;
|
|
}
|
|
|
|
/**
|
|
* Returns the list of session IDs that should be ignored by the InitialtionListener
|
|
*
|
|
* @return list of session IDs
|
|
*/
|
|
protected List<String> getIgnoredBytestreamRequests() {
|
|
return ignoredBytestreamRequests;
|
|
}
|
|
|
|
/**
|
|
* Disables the InBandBytestreamManager by removing its packet listeners and resetting its
|
|
* internal status, which includes removing this instance from the managers map.
|
|
*/
|
|
private void disableService() {
|
|
|
|
// remove manager from static managers map
|
|
managers.remove(connection);
|
|
|
|
// remove all listeners registered by this manager
|
|
this.connection.removePacketListener(this.initiationListener);
|
|
this.connection.removePacketListener(this.dataListener);
|
|
this.connection.removePacketListener(this.closeListener);
|
|
|
|
// shutdown threads
|
|
this.initiationListener.shutdown();
|
|
|
|
// reset internal status
|
|
this.userListeners.clear();
|
|
this.allRequestListeners.clear();
|
|
this.sessions.clear();
|
|
this.ignoredBytestreamRequests.clear();
|
|
|
|
}
|
|
|
|
}
|