1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2025-12-07 11:31:10 +01:00

merged branch improve_bytestreams in trunk

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@11821 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
Henning Staib 2010-08-15 11:57:11 +00:00 committed by henning
parent ef74695a1b
commit 8b54f34153
74 changed files with 11866 additions and 1902 deletions

View file

@ -0,0 +1,75 @@
/**
* 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 org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.IQTypeFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
/**
* CloseListener handles all In-Band Bytestream close requests.
* <p>
* If a close request is received it looks if a stored In-Band Bytestream
* session exists and closes it. If no session with the given session ID exists
* an &lt;item-not-found/&gt; error is returned to the sender.
*
* @author Henning Staib
*/
class CloseListener implements PacketListener {
/* manager containing the listeners and the XMPP connection */
private final InBandBytestreamManager manager;
/* packet filter for all In-Band Bytestream close requests */
private final PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(
Close.class), new IQTypeFilter(IQ.Type.SET));
/**
* Constructor.
*
* @param manager the In-Band Bytestream manager
*/
protected CloseListener(InBandBytestreamManager manager) {
this.manager = manager;
}
public void processPacket(Packet packet) {
Close closeRequest = (Close) packet;
InBandBytestreamSession ibbSession = this.manager.getSessions().get(
closeRequest.getSessionID());
if (ibbSession == null) {
this.manager.replyItemNotFoundPacket(closeRequest);
}
else {
ibbSession.closeByPeer(closeRequest);
this.manager.getSessions().remove(closeRequest.getSessionID());
}
}
/**
* Returns the packet filter for In-Band Bytestream close requests.
*
* @return the packet filter for In-Band Bytestream close requests
*/
protected PacketFilter getFilter() {
return this.closeFilter;
}
}

View file

@ -0,0 +1,73 @@
/**
* 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 org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
/**
* DataListener handles all In-Band Bytestream IQ stanzas containing a data
* packet extension that don't belong to an existing session.
* <p>
* If a data packet is received it looks if a stored In-Band Bytestream session
* exists. If no session with the given session ID exists an
* &lt;item-not-found/&gt; error is returned to the sender.
* <p>
* Data packets belonging to a running In-Band Bytestream session are processed
* by more specific listeners registered when an {@link InBandBytestreamSession}
* is created.
*
* @author Henning Staib
*/
class DataListener implements PacketListener {
/* manager containing the listeners and the XMPP connection */
private final InBandBytestreamManager manager;
/* packet filter for all In-Band Bytestream data packets */
private final PacketFilter dataFilter = new AndFilter(
new PacketTypeFilter(Data.class));
/**
* Constructor.
*
* @param manager the In-Band Bytestream manager
*/
public DataListener(InBandBytestreamManager manager) {
this.manager = manager;
}
public void processPacket(Packet packet) {
Data data = (Data) packet;
InBandBytestreamSession ibbSession = this.manager.getSessions().get(
data.getDataPacketExtension().getSessionID());
if (ibbSession == null) {
this.manager.replyItemNotFoundPacket(data);
}
}
/**
* Returns the packet filter for In-Band Bytestream data packets.
*
* @return the packet filter for In-Band Bytestream data packets
*/
protected PacketFilter getFilter() {
return this.dataFilter;
}
}

View file

@ -0,0 +1,46 @@
/**
* 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 org.jivesoftware.smackx.bytestreams.BytestreamListener;
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
/**
* InBandBytestreamListener are informed if a remote user wants to initiate an In-Band Bytestream.
* Implement this interface to handle incoming In-Band Bytestream requests.
* <p>
* There are two ways to add this listener. See
* {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
* {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
* further details.
*
* @author Henning Staib
*/
public abstract class InBandBytestreamListener implements BytestreamListener {
public void incomingBytestreamRequest(BytestreamRequest request) {
incomingBytestreamRequest((InBandBytestreamRequest) request);
}
/**
* This listener is notified if an In-Band Bytestream request from another user has been
* received.
*
* @param request the incoming In-Band Bytestream request
*/
public abstract void incomingBytestreamRequest(InBandBytestreamRequest request);
}

View file

@ -0,0 +1,546 @@
/**
* 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.smackx.bytestreams.BytestreamListener;
import org.jivesoftware.smackx.bytestreams.BytestreamManager;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
import org.jivesoftware.smackx.filetransfer.FileTransferManager;
import org.jivesoftware.smackx.packet.SyncPacketSend;
/**
* 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 &lt;not-acceptable/&gt; 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
* &lt;not-acceptable/&gt; 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
* &lt;not-acceptable/&gt; 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
* &lt;resource-constraint/&gt; 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
* &lt;resource-constraint/&gt; 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();
}
}

View file

@ -0,0 +1,92 @@
/**
* 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 org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
/**
* InBandBytestreamRequest class handles incoming In-Band Bytestream requests.
*
* @author Henning Staib
*/
public class InBandBytestreamRequest implements BytestreamRequest {
/* the bytestream initialization request */
private final Open byteStreamRequest;
/*
* In-Band Bytestream manager containing the XMPP connection and helper
* methods
*/
private final InBandBytestreamManager manager;
protected InBandBytestreamRequest(InBandBytestreamManager manager,
Open byteStreamRequest) {
this.manager = manager;
this.byteStreamRequest = byteStreamRequest;
}
/**
* Returns the sender of the In-Band Bytestream open request.
*
* @return the sender of the In-Band Bytestream open request
*/
public String getFrom() {
return this.byteStreamRequest.getFrom();
}
/**
* Returns the session ID of the In-Band Bytestream open request.
*
* @return the session ID of the In-Band Bytestream open request
*/
public String getSessionID() {
return this.byteStreamRequest.getSessionID();
}
/**
* Accepts the In-Band Bytestream open request and returns the session to
* send/receive data.
*
* @return the session to send/receive data
* @throws XMPPException if stream is invalid.
*/
public InBandBytestreamSession accept() throws XMPPException {
Connection connection = this.manager.getConnection();
// create In-Band Bytestream session and store it
InBandBytestreamSession ibbSession = new InBandBytestreamSession(connection,
this.byteStreamRequest, this.byteStreamRequest.getFrom());
this.manager.getSessions().put(this.byteStreamRequest.getSessionID(), ibbSession);
// acknowledge request
IQ resultIQ = IQ.createResultIQ(this.byteStreamRequest);
connection.sendPacket(resultIQ);
return ibbSession;
}
/**
* Rejects the In-Band Bytestream request by sending a reject error to the
* initiator.
*/
public void reject() {
this.manager.replyRejectPacket(this.byteStreamRequest);
}
}

View file

@ -0,0 +1,795 @@
/**
* 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.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.bytestreams.BytestreamSession;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
import org.jivesoftware.smackx.packet.SyncPacketSend;
/**
* InBandBytestreamSession class represents an In-Band Bytestream session.
* <p>
* In-band bytestreams are bidirectional and this session encapsulates the streams for both
* directions.
* <p>
* Note that closing the In-Band Bytestream session will close both streams. If both streams are
* closed individually the session will be closed automatically once the second stream is closed.
* Use the {@link #setCloseBothStreamsEnabled(boolean)} method if both streams should be closed
* automatically if one of them is closed.
*
* @author Henning Staib
*/
public class InBandBytestreamSession implements BytestreamSession {
/* XMPP connection */
private final Connection connection;
/* the In-Band Bytestream open request for this session */
private final Open byteStreamRequest;
/*
* the input stream for this session (either IQIBBInputStream or MessageIBBInputStream)
*/
private IBBInputStream inputStream;
/*
* the output stream for this session (either IQIBBOutputStream or MessageIBBOutputStream)
*/
private IBBOutputStream outputStream;
/* JID of the remote peer */
private String remoteJID;
/* flag to close both streams if one of them is closed */
private boolean closeBothStreamsEnabled = false;
/* flag to indicate if session is closed */
private boolean isClosed = false;
/**
* Constructor.
*
* @param connection the XMPP connection
* @param byteStreamRequest the In-Band Bytestream open request for this session
* @param remoteJID JID of the remote peer
*/
protected InBandBytestreamSession(Connection connection, Open byteStreamRequest,
String remoteJID) {
this.connection = connection;
this.byteStreamRequest = byteStreamRequest;
this.remoteJID = remoteJID;
// initialize streams dependent to the uses stanza type
switch (byteStreamRequest.getStanza()) {
case IQ:
this.inputStream = new IQIBBInputStream();
this.outputStream = new IQIBBOutputStream();
break;
case MESSAGE:
this.inputStream = new MessageIBBInputStream();
this.outputStream = new MessageIBBOutputStream();
break;
}
}
public InputStream getInputStream() {
return this.inputStream;
}
public OutputStream getOutputStream() {
return this.outputStream;
}
public int getReadTimeout() {
return this.inputStream.readTimeout;
}
public void setReadTimeout(int timeout) {
if (timeout < 0) {
throw new IllegalArgumentException("Timeout must be >= 0");
}
this.inputStream.readTimeout = timeout;
}
/**
* Returns whether both streams should be closed automatically if one of the streams is closed.
* Default is <code>false</code>.
*
* @return <code>true</code> if both streams will be closed if one of the streams is closed,
* <code>false</code> if both streams can be closed independently.
*/
public boolean isCloseBothStreamsEnabled() {
return closeBothStreamsEnabled;
}
/**
* Sets whether both streams should be closed automatically if one of the streams is closed.
* Default is <code>false</code>.
*
* @param closeBothStreamsEnabled <code>true</code> if both streams should be closed if one of
* the streams is closed, <code>false</code> if both streams should be closed
* independently
*/
public void setCloseBothStreamsEnabled(boolean closeBothStreamsEnabled) {
this.closeBothStreamsEnabled = closeBothStreamsEnabled;
}
public void close() throws IOException {
closeByLocal(true); // close input stream
closeByLocal(false); // close output stream
}
/**
* This method is invoked if a request to close the In-Band Bytestream has been received.
*
* @param closeRequest the close request from the remote peer
*/
protected void closeByPeer(Close closeRequest) {
/*
* close streams without flushing them, because stream is already considered closed on the
* remote peers side
*/
this.inputStream.closeInternal();
this.inputStream.cleanup();
this.outputStream.closeInternal(false);
// acknowledge close request
IQ confirmClose = IQ.createResultIQ(closeRequest);
this.connection.sendPacket(confirmClose);
}
/**
* This method is invoked if one of the streams has been closed locally, if an error occurred
* locally or if the whole session should be closed.
*
* @throws IOException if an error occurs while sending the close request
*/
protected synchronized void closeByLocal(boolean in) throws IOException {
if (this.isClosed) {
return;
}
if (this.closeBothStreamsEnabled) {
this.inputStream.closeInternal();
this.outputStream.closeInternal(true);
}
else {
if (in) {
this.inputStream.closeInternal();
}
else {
// close stream but try to send any data left
this.outputStream.closeInternal(true);
}
}
if (this.inputStream.isClosed && this.outputStream.isClosed) {
this.isClosed = true;
// send close request
Close close = new Close(this.byteStreamRequest.getSessionID());
close.setTo(this.remoteJID);
try {
SyncPacketSend.getReply(this.connection, close);
}
catch (XMPPException e) {
throw new IOException("Error while closing stream: " + e.getMessage());
}
this.inputStream.cleanup();
// remove session from manager
InBandBytestreamManager.getByteStreamManager(this.connection).getSessions().remove(this);
}
}
/**
* IBBInputStream class is the base implementation of an In-Band Bytestream input stream.
* Subclasses of this input stream must provide a packet listener along with a packet filter to
* collect the In-Band Bytestream data packets.
*/
private abstract class IBBInputStream extends InputStream {
/* the data packet listener to fill the data queue */
private final PacketListener dataPacketListener;
/* queue containing received In-Band Bytestream data packets */
protected final BlockingQueue<DataPacketExtension> dataQueue = new LinkedBlockingQueue<DataPacketExtension>();
/* buffer containing the data from one data packet */
private byte[] buffer;
/* pointer to the next byte to read from buffer */
private int bufferPointer = -1;
/* data packet sequence (range from 0 to 65535) */
private long seq = -1;
/* flag to indicate if input stream is closed */
private boolean isClosed = false;
/* flag to indicate if close method was invoked */
private boolean closeInvoked = false;
/* timeout for read operations */
private int readTimeout = 0;
/**
* Constructor.
*/
public IBBInputStream() {
// add data packet listener to connection
this.dataPacketListener = getDataPacketListener();
connection.addPacketListener(this.dataPacketListener, getDataPacketFilter());
}
/**
* Returns the packet listener that processes In-Band Bytestream data packets.
*
* @return the data packet listener
*/
protected abstract PacketListener getDataPacketListener();
/**
* Returns the packet filter that accepts In-Band Bytestream data packets.
*
* @return the data packet filter
*/
protected abstract PacketFilter getDataPacketFilter();
public synchronized int read() throws IOException {
checkClosed();
// if nothing read yet or whole buffer has been read fill buffer
if (bufferPointer == -1 || bufferPointer >= buffer.length) {
// if no data available and stream was closed return -1
if (!loadBuffer()) {
return -1;
}
}
// return byte and increment buffer pointer
return (int) buffer[bufferPointer++];
}
public synchronized int read(byte[] b, int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
}
else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
|| ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
}
else if (len == 0) {
return 0;
}
checkClosed();
// if nothing read yet or whole buffer has been read fill buffer
if (bufferPointer == -1 || bufferPointer >= buffer.length) {
// if no data available and stream was closed return -1
if (!loadBuffer()) {
return -1;
}
}
// if more bytes wanted than available return all available
int bytesAvailable = buffer.length - bufferPointer;
if (len > bytesAvailable) {
len = bytesAvailable;
}
System.arraycopy(buffer, bufferPointer, b, off, len);
bufferPointer += len;
return len;
}
public synchronized int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
/**
* This method blocks until a data packet is received, the stream is closed or the current
* thread is interrupted.
*
* @return <code>true</code> if data was received, otherwise <code>false</code>
* @throws IOException if data packets are out of sequence
*/
private synchronized boolean loadBuffer() throws IOException {
// wait until data is available or stream is closed
DataPacketExtension data = null;
try {
if (this.readTimeout == 0) {
while (data == null) {
if (isClosed && this.dataQueue.isEmpty()) {
return false;
}
data = this.dataQueue.poll(1000, TimeUnit.MILLISECONDS);
}
}
else {
data = this.dataQueue.poll(this.readTimeout, TimeUnit.MILLISECONDS);
if (data == null) {
throw new SocketTimeoutException();
}
}
}
catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
return false;
}
// handle sequence overflow
if (this.seq == 65535) {
this.seq = -1;
}
// check if data packets sequence is successor of last seen sequence
long seq = data.getSeq();
if (seq - 1 != this.seq) {
// packets out of order; close stream/session
InBandBytestreamSession.this.close();
throw new IOException("Packets out of sequence");
}
else {
this.seq = seq;
}
// set buffer to decoded data
buffer = data.getDecodedData();
bufferPointer = 0;
return true;
}
/**
* Checks if this stream is closed and throws an IOException if necessary
*
* @throws IOException if stream is closed and no data should be read anymore
*/
private void checkClosed() throws IOException {
/* throw no exception if there is data available, but not if close method was invoked */
if ((isClosed && this.dataQueue.isEmpty()) || closeInvoked) {
// clear data queue in case additional data was received after stream was closed
this.dataQueue.clear();
throw new IOException("Stream is closed");
}
}
public boolean markSupported() {
return false;
}
public void close() throws IOException {
if (isClosed) {
return;
}
this.closeInvoked = true;
InBandBytestreamSession.this.closeByLocal(true);
}
/**
* This method sets the close flag and removes the data packet listener.
*/
private void closeInternal() {
if (isClosed) {
return;
}
isClosed = true;
}
/**
* Invoked if the session is closed.
*/
private void cleanup() {
connection.removePacketListener(this.dataPacketListener);
}
}
/**
* IQIBBInputStream class implements IBBInputStream to be used with IQ stanzas encapsulating the
* data packets.
*/
private class IQIBBInputStream extends IBBInputStream {
protected PacketListener getDataPacketListener() {
return new PacketListener() {
private long lastSequence = -1;
public void processPacket(Packet packet) {
// get data packet extension
DataPacketExtension data = (DataPacketExtension) packet.getExtension(
DataPacketExtension.ELEMENT_NAME,
InBandBytestreamManager.NAMESPACE);
/*
* check if sequence was not used already (see XEP-0047 Section 2.2)
*/
if (data.getSeq() <= this.lastSequence) {
IQ unexpectedRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
XMPPError.Condition.unexpected_request));
connection.sendPacket(unexpectedRequest);
return;
}
// check if encoded data is valid (see XEP-0047 Section 2.2)
if (data.getDecodedData() == null) {
// data is invalid; respond with bad-request error
IQ badRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
XMPPError.Condition.bad_request));
connection.sendPacket(badRequest);
return;
}
// data is valid; add to data queue
dataQueue.offer(data);
// confirm IQ
IQ confirmData = IQ.createResultIQ((IQ) packet);
connection.sendPacket(confirmData);
// set last seen sequence
this.lastSequence = data.getSeq();
if (this.lastSequence == 65535) {
this.lastSequence = -1;
}
}
};
}
protected PacketFilter getDataPacketFilter() {
/*
* filter all IQ stanzas having type 'SET' (represented by Data class), containing a
* data packet extension, matching session ID and recipient
*/
return new AndFilter(new PacketTypeFilter(Data.class), new IBBDataPacketFilter());
}
}
/**
* MessageIBBInputStream class implements IBBInputStream to be used with message stanzas
* encapsulating the data packets.
*/
private class MessageIBBInputStream extends IBBInputStream {
protected PacketListener getDataPacketListener() {
return new PacketListener() {
public void processPacket(Packet packet) {
// get data packet extension
DataPacketExtension data = (DataPacketExtension) packet.getExtension(
DataPacketExtension.ELEMENT_NAME,
InBandBytestreamManager.NAMESPACE);
// check if encoded data is valid
if (data.getDecodedData() == null) {
/*
* TODO once a majority of XMPP server implementation support XEP-0079
* Advanced Message Processing the invalid message could be answered with an
* appropriate error. For now we just ignore the packet. Subsequent packets
* with an increased sequence will cause the input stream to close the
* stream/session.
*/
return;
}
// data is valid; add to data queue
dataQueue.offer(data);
// TODO confirm packet once XMPP servers support XEP-0079
}
};
}
@Override
protected PacketFilter getDataPacketFilter() {
/*
* filter all message stanzas containing a data packet extension, matching session ID
* and recipient
*/
return new AndFilter(new PacketTypeFilter(Message.class), new IBBDataPacketFilter());
}
}
/**
* IBBDataPacketFilter class filters all packets from the remote peer of this session,
* containing an In-Band Bytestream data packet extension whose session ID matches this sessions
* ID.
*/
private class IBBDataPacketFilter implements PacketFilter {
public boolean accept(Packet packet) {
// sender equals remote peer
if (!packet.getFrom().equalsIgnoreCase(remoteJID)) {
return false;
}
// stanza contains data packet extension
PacketExtension packetExtension = packet.getExtension(DataPacketExtension.ELEMENT_NAME,
InBandBytestreamManager.NAMESPACE);
if (packetExtension == null || !(packetExtension instanceof DataPacketExtension)) {
return false;
}
// session ID equals this session ID
DataPacketExtension data = (DataPacketExtension) packetExtension;
if (!data.getSessionID().equals(byteStreamRequest.getSessionID())) {
return false;
}
return true;
}
}
/**
* IBBOutputStream class is the base implementation of an In-Band Bytestream output stream.
* Subclasses of this output stream must provide a method to send data over XMPP stream.
*/
private abstract class IBBOutputStream extends OutputStream {
/* buffer with the size of this sessions block size */
protected final byte[] buffer;
/* pointer to next byte to write to buffer */
protected int bufferPointer = 0;
/* data packet sequence (range from 0 to 65535) */
protected long seq = 0;
/* flag to indicate if output stream is closed */
protected boolean isClosed = false;
/**
* Constructor.
*/
public IBBOutputStream() {
this.buffer = new byte[byteStreamRequest.getBlockSize()];
}
/**
* Writes the given data packet to the XMPP stream.
*
* @param data the data packet
* @throws IOException if an I/O error occurred while sending or if the stream is closed
*/
protected abstract void writeToXML(DataPacketExtension data) throws IOException;
public synchronized void write(int b) throws IOException {
if (this.isClosed) {
throw new IOException("Stream is closed");
}
// if buffer is full flush buffer
if (bufferPointer >= buffer.length) {
flushBuffer();
}
buffer[bufferPointer++] = (byte) b;
}
public synchronized void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
}
else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
|| ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
}
else if (len == 0) {
return;
}
if (this.isClosed) {
throw new IOException("Stream is closed");
}
// is data to send greater than buffer size
if (len >= buffer.length) {
// "byte" off the first chunk to write out
writeOut(b, off, buffer.length);
// recursively call this method with the lesser amount
write(b, off + buffer.length, len - buffer.length);
}
else {
writeOut(b, off, len);
}
}
public synchronized void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
/**
* Fills the buffer with the given data and sends it over the XMPP stream if the buffers
* capacity has been reached. This method is only called from this class so it is assured
* that the amount of data to send is <= buffer capacity
*
* @param b the data
* @param off the data
* @param len the number of bytes to write
* @throws IOException if an I/O error occurred while sending or if the stream is closed
*/
private synchronized void writeOut(byte b[], int off, int len) throws IOException {
if (this.isClosed) {
throw new IOException("Stream is closed");
}
// set to 0 in case the next 'if' block is not executed
int available = 0;
// is data to send greater that buffer space left
if (len > buffer.length - bufferPointer) {
// fill buffer to capacity and send it
available = buffer.length - bufferPointer;
System.arraycopy(b, off, buffer, bufferPointer, available);
bufferPointer += available;
flushBuffer();
}
// copy the data left to buffer
System.arraycopy(b, off + available, buffer, bufferPointer, len - available);
bufferPointer += len - available;
}
public synchronized void flush() throws IOException {
if (this.isClosed) {
throw new IOException("Stream is closed");
}
flushBuffer();
}
private synchronized void flushBuffer() throws IOException {
// do nothing if no data to send available
if (bufferPointer == 0) {
return;
}
// create data packet
String enc = StringUtils.encodeBase64(buffer, 0, bufferPointer, false);
DataPacketExtension data = new DataPacketExtension(byteStreamRequest.getSessionID(),
this.seq, enc);
// write to XMPP stream
writeToXML(data);
// reset buffer pointer
bufferPointer = 0;
// increment sequence, considering sequence overflow
this.seq = (this.seq + 1 == 65535 ? 0 : this.seq + 1);
}
public void close() throws IOException {
if (isClosed) {
return;
}
InBandBytestreamSession.this.closeByLocal(false);
}
/**
* Sets the close flag and optionally flushes the stream.
*
* @param flush if <code>true</code> flushes the stream
*/
protected void closeInternal(boolean flush) {
if (this.isClosed) {
return;
}
this.isClosed = true;
try {
if (flush) {
flushBuffer();
}
}
catch (IOException e) {
/*
* ignore, because writeToXML() will not throw an exception if stream is already
* closed
*/
}
}
}
/**
* IQIBBOutputStream class implements IBBOutputStream to be used with IQ stanzas encapsulating
* the data packets.
*/
private class IQIBBOutputStream extends IBBOutputStream {
@Override
protected synchronized void writeToXML(DataPacketExtension data) throws IOException {
// create IQ stanza containing data packet
IQ iq = new Data(data);
iq.setTo(remoteJID);
try {
SyncPacketSend.getReply(connection, iq);
}
catch (XMPPException e) {
// close session unless it is already closed
if (!this.isClosed) {
InBandBytestreamSession.this.close();
throw new IOException("Error while sending Data: " + e.getMessage());
}
}
}
}
/**
* MessageIBBOutputStream class implements IBBOutputStream to be used with message stanzas
* encapsulating the data packets.
*/
private class MessageIBBOutputStream extends IBBOutputStream {
@Override
protected synchronized void writeToXML(DataPacketExtension data) {
// create message stanza containing data packet
Message message = new Message(remoteJID);
message.addExtension(data);
connection.sendPacket(message);
}
}
}

View file

@ -0,0 +1,127 @@
/**
* 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.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.IQTypeFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smackx.bytestreams.BytestreamListener;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
/**
* InitiationListener handles all incoming In-Band Bytestream open requests. If there are no
* listeners for a In-Band Bytestream request InitiationListener will always refuse the request and
* reply with a &lt;not-acceptable/&gt; error (<a
* href="http://xmpp.org/extensions/xep-0047.html#example-5" >XEP-0047</a> Section 2.1).
* <p>
* All In-Band Bytestream request having a block size greater than the maximum allowed block size
* for this connection are rejected with an &lt;resource-constraint/&gt; error. The maximum block
* size can be set by invoking {@link InBandBytestreamManager#setMaximumBlockSize(int)}.
*
* @author Henning Staib
*/
class InitiationListener implements PacketListener {
/* manager containing the listeners and the XMPP connection */
private final InBandBytestreamManager manager;
/* packet filter for all In-Band Bytestream requests */
private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Open.class),
new IQTypeFilter(IQ.Type.SET));
/* executor service to process incoming requests concurrently */
private final ExecutorService initiationListenerExecutor;
/**
* Constructor.
*
* @param manager the In-Band Bytestream manager
*/
protected InitiationListener(InBandBytestreamManager manager) {
this.manager = manager;
initiationListenerExecutor = Executors.newCachedThreadPool();
}
public void processPacket(final Packet packet) {
initiationListenerExecutor.execute(new Runnable() {
public void run() {
processRequest(packet);
}
});
}
private void processRequest(Packet packet) {
Open ibbRequest = (Open) packet;
// validate that block size is within allowed range
if (ibbRequest.getBlockSize() > this.manager.getMaximumBlockSize()) {
this.manager.replyResourceConstraintPacket(ibbRequest);
return;
}
// ignore request if in ignore list
if (this.manager.getIgnoredBytestreamRequests().remove(ibbRequest.getSessionID()))
return;
// build bytestream request from packet
InBandBytestreamRequest request = new InBandBytestreamRequest(this.manager, ibbRequest);
// notify listeners for bytestream initiation from a specific user
BytestreamListener userListener = this.manager.getUserListener(ibbRequest.getFrom());
if (userListener != null) {
userListener.incomingBytestreamRequest(request);
}
else if (!this.manager.getAllRequestListeners().isEmpty()) {
/*
* if there is no user specific listener inform listeners for all initiation requests
*/
for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
listener.incomingBytestreamRequest(request);
}
}
else {
/*
* if there is no listener for this initiation request, reply with reject message
*/
this.manager.replyRejectPacket(ibbRequest);
}
}
/**
* Returns the packet filter for In-Band Bytestream open requests.
*
* @return the packet filter for In-Band Bytestream open requests
*/
protected PacketFilter getFilter() {
return this.initFilter;
}
/**
* Shuts down the listeners executor service.
*/
protected void shutdown() {
this.initiationListenerExecutor.shutdownNow();
}
}

View file

@ -0,0 +1,65 @@
/**
* 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.packet;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
/**
* Represents a request to close an In-Band Bytestream.
*
* @author Henning Staib
*/
public class Close extends IQ {
/* unique session ID identifying this In-Band Bytestream */
private final String sessionID;
/**
* Creates a new In-Band Bytestream close request packet.
*
* @param sessionID unique session ID identifying this In-Band Bytestream
*/
public Close(String sessionID) {
if (sessionID == null || "".equals(sessionID)) {
throw new IllegalArgumentException("Session ID must not be null or empty");
}
this.sessionID = sessionID;
setType(Type.SET);
}
/**
* Returns the unique session ID identifying this In-Band Bytestream.
*
* @return the unique session ID identifying this In-Band Bytestream
*/
public String getSessionID() {
return sessionID;
}
@Override
public String getChildElementXML() {
StringBuilder buf = new StringBuilder();
buf.append("<close ");
buf.append("xmlns=\"");
buf.append(InBandBytestreamManager.NAMESPACE);
buf.append("\" ");
buf.append("sid=\"");
buf.append(sessionID);
buf.append("\"");
buf.append("/>");
return buf.toString();
}
}

View file

@ -0,0 +1,64 @@
/**
* 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.packet;
import org.jivesoftware.smack.packet.IQ;
/**
* Represents a chunk of data sent over an In-Band Bytestream encapsulated in an
* IQ stanza.
*
* @author Henning Staib
*/
public class Data extends IQ {
/* the data packet extension */
private final DataPacketExtension dataPacketExtension;
/**
* Constructor.
*
* @param data data packet extension containing the encoded data
*/
public Data(DataPacketExtension data) {
if (data == null) {
throw new IllegalArgumentException("Data must not be null");
}
this.dataPacketExtension = data;
/*
* also set as packet extension so that data packet extension can be
* retrieved from IQ stanza and message stanza in the same way
*/
addExtension(data);
setType(IQ.Type.SET);
}
/**
* Returns the data packet extension.
* <p>
* Convenience method for <code>packet.getExtension("data",
* "http://jabber.org/protocol/ibb")</code>.
*
* @return the data packet extension
*/
public DataPacketExtension getDataPacketExtension() {
return this.dataPacketExtension;
}
public String getChildElementXML() {
return this.dataPacketExtension.toXML();
}
}

View file

@ -0,0 +1,149 @@
/**
* 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.packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
/**
* Represents a chunk of data of an In-Band Bytestream within an IQ stanza or a
* message stanza
*
* @author Henning Staib
*/
public class DataPacketExtension implements PacketExtension {
/**
* The element name of the data packet extension.
*/
public final static String ELEMENT_NAME = "data";
/* unique session ID identifying this In-Band Bytestream */
private final String sessionID;
/* sequence of this packet in regard to the other data packets */
private final long seq;
/* the data contained in this packet */
private final String data;
private byte[] decodedData;
/**
* Creates a new In-Band Bytestream data packet.
*
* @param sessionID unique session ID identifying this In-Band Bytestream
* @param seq sequence of this packet in regard to the other data packets
* @param data the base64 encoded data contained in this packet
*/
public DataPacketExtension(String sessionID, long seq, String data) {
if (sessionID == null || "".equals(sessionID)) {
throw new IllegalArgumentException("Session ID must not be null or empty");
}
if (seq < 0 || seq > 65535) {
throw new IllegalArgumentException("Sequence must not be between 0 and 65535");
}
if (data == null) {
throw new IllegalArgumentException("Data must not be null");
}
this.sessionID = sessionID;
this.seq = seq;
this.data = data;
}
/**
* Returns the unique session ID identifying this In-Band Bytestream.
*
* @return the unique session ID identifying this In-Band Bytestream
*/
public String getSessionID() {
return sessionID;
}
/**
* Returns the sequence of this packet in regard to the other data packets.
*
* @return the sequence of this packet in regard to the other data packets.
*/
public long getSeq() {
return seq;
}
/**
* Returns the data contained in this packet.
*
* @return the data contained in this packet.
*/
public String getData() {
return data;
}
/**
* Returns the decoded data or null if data could not be decoded.
* <p>
* The encoded data is invalid if it contains bad Base64 input characters or
* if it contains the pad ('=') character on a position other than the last
* character(s) of the data. See <a
* href="http://xmpp.org/extensions/xep-0047.html#sec">XEP-0047</a> Section
* 6.
*
* @return the decoded data
*/
public byte[] getDecodedData() {
// return cached decoded data
if (this.decodedData != null) {
return this.decodedData;
}
// data must not contain the pad (=) other than end of data
if (data.matches(".*={1,2}+.+")) {
return null;
}
// decodeBase64 will return null if bad characters are included
this.decodedData = StringUtils.decodeBase64(data);
return this.decodedData;
}
public String getElementName() {
return ELEMENT_NAME;
}
public String getNamespace() {
return InBandBytestreamManager.NAMESPACE;
}
public String toXML() {
StringBuilder buf = new StringBuilder();
buf.append("<");
buf.append(getElementName());
buf.append(" ");
buf.append("xmlns=\"");
buf.append(InBandBytestreamManager.NAMESPACE);
buf.append("\" ");
buf.append("seq=\"");
buf.append(seq);
buf.append("\" ");
buf.append("sid=\"");
buf.append(sessionID);
buf.append("\">");
buf.append(data);
buf.append("</");
buf.append(getElementName());
buf.append(">");
return buf.toString();
}
}

View file

@ -0,0 +1,126 @@
/**
* 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.packet;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;
/**
* Represents a request to open an In-Band Bytestream.
*
* @author Henning Staib
*/
public class Open extends IQ {
/* unique session ID identifying this In-Band Bytestream */
private final String sessionID;
/* block size in which the data will be fragmented */
private final int blockSize;
/* stanza type used to encapsulate the data */
private final StanzaType stanza;
/**
* Creates a new In-Band Bytestream open request packet.
* <p>
* The data sent over this In-Band Bytestream will be fragmented in blocks
* with the given block size. The block size should not be greater than
* 65535. A recommended default value is 4096.
* <p>
* The data can be sent using IQ stanzas or message stanzas.
*
* @param sessionID unique session ID identifying this In-Band Bytestream
* @param blockSize block size in which the data will be fragmented
* @param stanza stanza type used to encapsulate the data
*/
public Open(String sessionID, int blockSize, StanzaType stanza) {
if (sessionID == null || "".equals(sessionID)) {
throw new IllegalArgumentException("Session ID must not be null or empty");
}
if (blockSize <= 0) {
throw new IllegalArgumentException("Block size must be greater than zero");
}
this.sessionID = sessionID;
this.blockSize = blockSize;
this.stanza = stanza;
setType(Type.SET);
}
/**
* Creates a new In-Band Bytestream open request packet.
* <p>
* The data sent over this In-Band Bytestream will be fragmented in blocks
* with the given block size. The block size should not be greater than
* 65535. A recommended default value is 4096.
* <p>
* The data will be sent using IQ stanzas.
*
* @param sessionID unique session ID identifying this In-Band Bytestream
* @param blockSize block size in which the data will be fragmented
*/
public Open(String sessionID, int blockSize) {
this(sessionID, blockSize, StanzaType.IQ);
}
/**
* Returns the unique session ID identifying this In-Band Bytestream.
*
* @return the unique session ID identifying this In-Band Bytestream
*/
public String getSessionID() {
return sessionID;
}
/**
* Returns the block size in which the data will be fragmented.
*
* @return the block size in which the data will be fragmented
*/
public int getBlockSize() {
return blockSize;
}
/**
* Returns the stanza type used to encapsulate the data.
*
* @return the stanza type used to encapsulate the data
*/
public StanzaType getStanza() {
return stanza;
}
@Override
public String getChildElementXML() {
StringBuilder buf = new StringBuilder();
buf.append("<open ");
buf.append("xmlns=\"");
buf.append(InBandBytestreamManager.NAMESPACE);
buf.append("\" ");
buf.append("block-size=\"");
buf.append(blockSize);
buf.append("\" ");
buf.append("sid=\"");
buf.append(sessionID);
buf.append("\" ");
buf.append("stanza=\"");
buf.append(stanza.toString().toLowerCase());
buf.append("\"");
buf.append("/>");
return buf.toString();
}
}

View file

@ -0,0 +1,33 @@
/**
* 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.provider;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
import org.xmlpull.v1.XmlPullParser;
/**
* Parses a close In-Band Bytestream packet.
*
* @author Henning Staib
*/
public class CloseIQProvider implements IQProvider {
public IQ parseIQ(XmlPullParser parser) throws Exception {
String sid = parser.getAttributeValue("", "sid");
return new Close(sid);
}
}

View file

@ -0,0 +1,45 @@
/**
* 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.provider;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
import org.xmlpull.v1.XmlPullParser;
/**
* Parses an In-Band Bytestream data packet which can be a packet extension of
* either an IQ stanza or a message stanza.
*
* @author Henning Staib
*/
public class DataPacketProvider implements PacketExtensionProvider, IQProvider {
public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
String sessionID = parser.getAttributeValue("", "sid");
long seq = Long.parseLong(parser.getAttributeValue("", "seq"));
String data = parser.nextText();
return new DataPacketExtension(sessionID, seq, data);
}
public IQ parseIQ(XmlPullParser parser) throws Exception {
DataPacketExtension data = (DataPacketExtension) parseExtension(parser);
IQ iq = new Data(data);
return iq;
}
}

View file

@ -0,0 +1,45 @@
/**
* 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.provider;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;
import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
import org.xmlpull.v1.XmlPullParser;
/**
* Parses an In-Band Bytestream open packet.
*
* @author Henning Staib
*/
public class OpenIQProvider implements IQProvider {
public IQ parseIQ(XmlPullParser parser) throws Exception {
String sessionID = parser.getAttributeValue("", "sid");
int blockSize = Integer.parseInt(parser.getAttributeValue("", "block-size"));
String stanzaValue = parser.getAttributeValue("", "stanza");
StanzaType stanza = null;
if (stanzaValue == null) {
stanza = StanzaType.IQ;
}
else {
stanza = StanzaType.valueOf(stanzaValue.toUpperCase());
}
return new Open(sessionID, blockSize, stanza);
}
}