/** | |
* 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(Connection connection) { | |
final InBandBytestreamManager manager; | |
manager = InBandBytestreamManager.getByteStreamManager(connection); | |
// register shutdown listener | |
connection.addConnectionListener(new AbstractConnectionListener() { | |
public void connectionClosed() { | |
manager.disableService(); | |
} | |
}); | |
} | |
}); | |
} | |
/** | |
* 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. | |
*/ | |
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(); | |
} | |
} |