FileTransferNegotiator.java
- /**
- *
- * Copyright 2003-2006 Jive Software.
- *
- * 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.filetransfer;
- import java.net.URLConnection;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.List;
- import java.util.Map;
- import java.util.Random;
- import java.util.WeakHashMap;
- import org.jivesoftware.smack.Manager;
- import org.jivesoftware.smack.SmackException.NoResponseException;
- import org.jivesoftware.smack.SmackException.NotConnectedException;
- import org.jivesoftware.smack.XMPPConnection;
- import org.jivesoftware.smack.XMPPException.XMPPErrorException;
- import org.jivesoftware.smack.packet.IQ;
- import org.jivesoftware.smack.packet.Stanza;
- import org.jivesoftware.smack.packet.XMPPError;
- import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
- import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
- import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
- import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms;
- import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException;
- import org.jivesoftware.smackx.si.packet.StreamInitiation;
- import org.jivesoftware.smackx.xdata.FormField;
- import org.jivesoftware.smackx.xdata.packet.DataForm;
- import org.jxmpp.jid.Jid;
- /**
- * Manages the negotiation of file transfers according to XEP-0096. If a file is
- * being sent the remote user chooses the type of stream under which the file
- * will be sent.
- *
- * @author Alexander Wenckus
- * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
- */
- public class FileTransferNegotiator extends Manager {
- public static final String SI_NAMESPACE = "http://jabber.org/protocol/si";
- public static final String SI_PROFILE_FILE_TRANSFER_NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer";
- private static final String[] NAMESPACE = { SI_NAMESPACE, SI_PROFILE_FILE_TRANSFER_NAMESPACE };
- private static final Map<XMPPConnection, FileTransferNegotiator> INSTANCES = new WeakHashMap<XMPPConnection, FileTransferNegotiator>();
- private static final String STREAM_INIT_PREFIX = "jsi_";
- protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
- private static final Random randomGenerator = new Random();
- /**
- * A static variable to use only offer IBB for file transfer. It is generally recommend to only
- * set this variable to true for testing purposes as IBB is the backup file transfer method
- * and shouldn't be used as the only transfer method in production systems.
- */
- public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;
- /**
- * Returns the file transfer negotiator related to a particular connection.
- * When this class is requested on a particular connection the file transfer
- * service is automatically enabled.
- *
- * @param connection The connection for which the transfer manager is desired
- * @return The FileTransferNegotiator
- */
- public static synchronized FileTransferNegotiator getInstanceFor(
- final XMPPConnection connection) {
- FileTransferNegotiator fileTransferNegotiator = INSTANCES.get(connection);
- if (fileTransferNegotiator == null) {
- fileTransferNegotiator = new FileTransferNegotiator(connection);
- INSTANCES.put(connection, fileTransferNegotiator);
- }
- return fileTransferNegotiator;
- }
- /**
- * Enable the Jabber services related to file transfer on the particular
- * connection.
- *
- * @param connection The connection on which to enable or disable the services.
- * @param isEnabled True to enable, false to disable.
- */
- private static void setServiceEnabled(final XMPPConnection connection,
- final boolean isEnabled) {
- ServiceDiscoveryManager manager = ServiceDiscoveryManager
- .getInstanceFor(connection);
- List<String> namespaces = new ArrayList<String>();
- namespaces.addAll(Arrays.asList(NAMESPACE));
- namespaces.add(DataPacketExtension.NAMESPACE);
- if (!IBB_ONLY) {
- namespaces.add(Bytestream.NAMESPACE);
- }
- for (String namespace : namespaces) {
- if (isEnabled) {
- manager.addFeature(namespace);
- } else {
- manager.removeFeature(namespace);
- }
- }
- }
- /**
- * Checks to see if all file transfer related services are enabled on the
- * connection.
- *
- * @param connection The connection to check
- * @return True if all related services are enabled, false if they are not.
- */
- public static boolean isServiceEnabled(final XMPPConnection connection) {
- ServiceDiscoveryManager manager = ServiceDiscoveryManager
- .getInstanceFor(connection);
- List<String> namespaces = new ArrayList<String>();
- namespaces.addAll(Arrays.asList(NAMESPACE));
- namespaces.add(DataPacketExtension.NAMESPACE);
- if (!IBB_ONLY) {
- namespaces.add(Bytestream.NAMESPACE);
- }
- for (String namespace : namespaces) {
- if (!manager.includesFeature(namespace)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Returns a collection of the supported transfer protocols.
- *
- * @return Returns a collection of the supported transfer protocols.
- */
- public static Collection<String> getSupportedProtocols() {
- List<String> protocols = new ArrayList<String>();
- protocols.add(DataPacketExtension.NAMESPACE);
- if (!IBB_ONLY) {
- protocols.add(Bytestream.NAMESPACE);
- }
- return Collections.unmodifiableList(protocols);
- }
- // non-static
- private final StreamNegotiator byteStreamTransferManager;
- private final StreamNegotiator inbandTransferManager;
- private FileTransferNegotiator(final XMPPConnection connection) {
- super(connection);
- byteStreamTransferManager = new Socks5TransferNegotiator(connection);
- inbandTransferManager = new IBBTransferNegotiator(connection);
- setServiceEnabled(connection, true);
- }
- /**
- * Selects an appropriate stream negotiator after examining the incoming file transfer request.
- *
- * @param request The related file transfer request.
- * @return The file transfer object that handles the transfer
- * @throws NoStreamMethodsOfferedException If there are either no stream methods contained in the packet, or
- * there is not an appropriate stream method.
- * @throws NotConnectedException
- * @throws NoAcceptableTransferMechanisms
- * @throws InterruptedException
- */
- public StreamNegotiator selectStreamNegotiator(
- FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException {
- StreamInitiation si = request.getStreamInitiation();
- FormField streamMethodField = getStreamMethodField(si
- .getFeatureNegotiationForm());
- if (streamMethodField == null) {
- String errorMessage = "No stream methods contained in stanza.";
- XMPPError error = XMPPError.from(XMPPError.Condition.bad_request, errorMessage);
- IQ iqPacket = IQ.createErrorResponse(si, error);
- connection().sendStanza(iqPacket);
- throw new FileTransferException.NoStreamMethodsOfferedException();
- }
- // select the appropriate protocol
- StreamNegotiator selectedStreamNegotiator;
- try {
- selectedStreamNegotiator = getNegotiator(streamMethodField);
- }
- catch (NoAcceptableTransferMechanisms e) {
- IQ iqPacket = IQ.createErrorResponse(si, XMPPError.from(XMPPError.Condition.bad_request, "No acceptable transfer mechanism"));
- connection().sendStanza(iqPacket);
- throw e;
- }
- // return the appropriate negotiator
- return selectedStreamNegotiator;
- }
- private FormField getStreamMethodField(DataForm form) {
- for (FormField field : form.getFields()) {
- if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {
- return field;
- }
- }
- return null;
- }
- private StreamNegotiator getNegotiator(final FormField field)
- throws NoAcceptableTransferMechanisms {
- String variable;
- boolean isByteStream = false;
- boolean isIBB = false;
- for (FormField.Option option : field.getOptions()) {
- variable = option.getValue();
- if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
- isByteStream = true;
- }
- else if (variable.equals(DataPacketExtension.NAMESPACE)) {
- isIBB = true;
- }
- }
- if (!isByteStream && !isIBB) {
- throw new FileTransferException.NoAcceptableTransferMechanisms();
- }
- if (isByteStream && isIBB) {
- return new FaultTolerantNegotiator(connection(),
- byteStreamTransferManager,
- inbandTransferManager);
- }
- else if (isByteStream) {
- return byteStreamTransferManager;
- }
- else {
- return inbandTransferManager;
- }
- }
- /**
- * Returns a new, unique, stream ID to identify a file transfer.
- *
- * @return Returns a new, unique, stream ID to identify a file transfer.
- */
- public String getNextStreamID() {
- StringBuilder buffer = new StringBuilder();
- buffer.append(STREAM_INIT_PREFIX);
- buffer.append(Math.abs(randomGenerator.nextLong()));
- return buffer.toString();
- }
- /**
- * Send a request to another user to send them a file. The other user has
- * the option of, accepting, rejecting, or not responding to a received file
- * transfer request.
- * <p/>
- * If they accept, the packet will contain the other user's chosen stream
- * type to send the file across. The two choices this implementation
- * provides to the other user for file transfer are <a
- * href="http://www.xmpp.org/extensions/jep-0065.html">SOCKS5 Bytestreams</a>,
- * which is the preferred method of transfer, and <a
- * href="http://www.xmpp.org/extensions/jep-0047.html">In-Band Bytestreams</a>,
- * which is the fallback mechanism.
- * <p/>
- * The other user may choose to decline the file request if they do not
- * desire the file, their client does not support XEP-0096, or if there are
- * no acceptable means to transfer the file.
- * <p/>
- * Finally, if the other user does not respond this method will return null
- * after the specified timeout.
- *
- * @param userID The userID of the user to whom the file will be sent.
- * @param streamID The unique identifier for this file transfer.
- * @param fileName The name of this file. Preferably it should include an
- * extension as it is used to determine what type of file it is.
- * @param size The size, in bytes, of the file.
- * @param desc A description of the file.
- * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
- * user to respond. If they do not respond in time, this
- * @return Returns the stream negotiator selected by the peer.
- * @throws XMPPErrorException Thrown if there is an error negotiating the file transfer.
- * @throws NotConnectedException
- * @throws NoResponseException
- * @throws NoAcceptableTransferMechanisms
- * @throws InterruptedException
- */
- public StreamNegotiator negotiateOutgoingTransfer(final Jid userID,
- final String streamID, final String fileName, final long size,
- final String desc, int responseTimeout) throws XMPPErrorException, NotConnectedException, NoResponseException, NoAcceptableTransferMechanisms, InterruptedException {
- StreamInitiation si = new StreamInitiation();
- si.setSessionID(streamID);
- si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
- StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
- siFile.setDesc(desc);
- si.setFile(siFile);
- si.setFeatureNegotiationForm(createDefaultInitiationForm());
- si.setFrom(connection().getUser());
- si.setTo(userID);
- si.setType(IQ.Type.set);
- Stanza siResponse = connection().createPacketCollectorAndSend(si).nextResultOrThrow(
- responseTimeout);
- if (siResponse instanceof IQ) {
- IQ iqResponse = (IQ) siResponse;
- if (iqResponse.getType().equals(IQ.Type.result)) {
- StreamInitiation response = (StreamInitiation) siResponse;
- return getOutgoingNegotiator(getStreamMethodField(response
- .getFeatureNegotiationForm()));
- }
- else {
- throw new XMPPErrorException(iqResponse.getError());
- }
- }
- else {
- return null;
- }
- }
- private StreamNegotiator getOutgoingNegotiator(final FormField field) throws NoAcceptableTransferMechanisms {
- boolean isByteStream = false;
- boolean isIBB = false;
- for (String variable : field.getValues()) {
- if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
- isByteStream = true;
- }
- else if (variable.equals(DataPacketExtension.NAMESPACE)) {
- isIBB = true;
- }
- }
- if (!isByteStream && !isIBB) {
- throw new FileTransferException.NoAcceptableTransferMechanisms();
- }
- if (isByteStream && isIBB) {
- return new FaultTolerantNegotiator(connection(),
- byteStreamTransferManager, inbandTransferManager);
- }
- else if (isByteStream) {
- return byteStreamTransferManager;
- }
- else {
- return inbandTransferManager;
- }
- }
- private DataForm createDefaultInitiationForm() {
- DataForm form = new DataForm(DataForm.Type.form);
- FormField field = new FormField(STREAM_DATA_FIELD_NAME);
- field.setType(FormField.Type.list_single);
- if (!IBB_ONLY) {
- field.addOption(new FormField.Option(Bytestream.NAMESPACE));
- }
- field.addOption(new FormField.Option(DataPacketExtension.NAMESPACE));
- form.addField(field);
- return form;
- }
- }