/** * $RCSfile$ * $Revision: $ * $Date: $ * * Copyright 2003-2006 Jive Software. * * 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.filetransfer; import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.PacketCollector; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.filter.PacketIDFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smackx.Form; import org.jivesoftware.smackx.FormField; import org.jivesoftware.smackx.ServiceDiscoveryManager; import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager; import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager; import org.jivesoftware.smackx.packet.DataForm; import org.jivesoftware.smackx.packet.StreamInitiation; /** * Manages the negotiation of file transfers according to JEP-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 { // Static private static final String[] NAMESPACE = { "http://jabber.org/protocol/si/profile/file-transfer", "http://jabber.org/protocol/si" }; private static final Map<Connection, FileTransferNegotiator> transferObject = new ConcurrentHashMap<Connection, 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; /** * A convenience method to create an IQ packet. * * @param ID * The packet ID of the * @param to * To whom the packet is addressed. * @param from * From whom the packet is sent. * @param type * The IQ type of the packet. * @return The created IQ packet. */ public static IQ createIQ(final String ID, final String to, final String from, final IQ.Type type) { final IQ iqPacket = new IQ() { @Override public String getChildElementXML() { return null; } }; iqPacket.setPacketID(ID); iqPacket.setTo(to); iqPacket.setFrom(from); iqPacket.setType(type); return iqPacket; } /** * 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 IMFileTransferManager */ public static FileTransferNegotiator getInstanceFor( final Connection connection) { if (connection == null) { throw new IllegalArgumentException("Connection cannot be null"); } if (!connection.isConnected()) { return null; } if (transferObject.containsKey(connection)) { return transferObject.get(connection); } else { final FileTransferNegotiator transfer = new FileTransferNegotiator( connection); setServiceEnabled(connection, true); transferObject.put(connection, transfer); return transfer; } } /** * Returns a collection of the supported transfer protocols. * * @return Returns a collection of the supported transfer protocols. */ public static Collection<String> getSupportedProtocols() { final List<String> protocols = new ArrayList<String>(); protocols.add(InBandBytestreamManager.NAMESPACE); if (!IBB_ONLY) { protocols.add(Socks5BytestreamManager.NAMESPACE); } return Collections.unmodifiableList(protocols); } /** * 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 Connection connection) { final ServiceDiscoveryManager manager = ServiceDiscoveryManager .getInstanceFor(connection); final List<String> namespaces = new ArrayList<String>(); namespaces.addAll(Arrays.asList(NAMESPACE)); namespaces.add(InBandBytestreamManager.NAMESPACE); if (!IBB_ONLY) { namespaces.add(Socks5BytestreamManager.NAMESPACE); } for (final String namespace : namespaces) { if (!manager.includesFeature(namespace)) { return false; } } return true; } /** * 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. */ public static void setServiceEnabled(final Connection connection, final boolean isEnabled) { final ServiceDiscoveryManager manager = ServiceDiscoveryManager .getInstanceFor(connection); final List<String> namespaces = new ArrayList<String>(); namespaces.addAll(Arrays.asList(NAMESPACE)); namespaces.add(InBandBytestreamManager.NAMESPACE); if (!IBB_ONLY) { namespaces.add(Socks5BytestreamManager.NAMESPACE); } for (final String namespace : namespaces) { if (isEnabled) { if (!manager.includesFeature(namespace)) { manager.addFeature(namespace); } } else { manager.removeFeature(namespace); } } } // non-static private final Connection connection; private final StreamNegotiator byteStreamTransferManager; private final StreamNegotiator inbandTransferManager; private FileTransferNegotiator(final Connection connection) { configureConnection(connection); this.connection = connection; byteStreamTransferManager = new Socks5TransferNegotiator(connection); inbandTransferManager = new IBBTransferNegotiator(connection); } private void cleanup(final Connection connection) { if (transferObject.remove(connection) != null) { inbandTransferManager.cleanup(); } } private void configureConnection(final Connection connection) { connection.addConnectionListener(new ConnectionListener() { @Override public void connectionClosed() { cleanup(connection); } @Override public void connectionClosedOnError(Exception e) { cleanup(connection); } @Override public void reconnectingIn(int seconds) { // ignore } @Override public void reconnectionFailed(Exception e) { // ignore } @Override public void reconnectionSuccessful() { // ignore } }); } private DataForm createDefaultInitiationForm() { final DataForm form = new DataForm(Form.TYPE_FORM); final FormField field = new FormField(STREAM_DATA_FIELD_NAME); field.setType(FormField.TYPE_LIST_SINGLE); if (!IBB_ONLY) { field.addOption(new FormField.Option( Socks5BytestreamManager.NAMESPACE)); } field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE)); form.addField(field); return form; } private StreamNegotiator getNegotiator(final FormField field) throws XMPPException { String variable; boolean isByteStream = false; boolean isIBB = false; for (final Iterator<FormField.Option> it = field.getOptions(); it .hasNext();) { variable = it.next().getValue(); if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) { isByteStream = true; } else if (variable.equals(InBandBytestreamManager.NAMESPACE)) { isIBB = true; } } if (!isByteStream && !isIBB) { final XMPPError error = new XMPPError( XMPPError.Condition.bad_request, "No acceptable transfer mechanism"); throw new XMPPException(error.getMessage(), error); } // if (isByteStream && isIBB && // field.getType().equals(FormField.TYPE_LIST_MULTI)) { 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() { final StringBuilder buffer = new StringBuilder(); buffer.append(STREAM_INIT_PREFIX); buffer.append(Math.abs(randomGenerator.nextLong())); return buffer.toString(); } private StreamNegotiator getOutgoingNegotiator(final FormField field) throws XMPPException { String variable; boolean isByteStream = false; boolean isIBB = false; for (final Iterator<String> it = field.getValues(); it.hasNext();) { variable = it.next(); if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) { isByteStream = true; } else if (variable.equals(InBandBytestreamManager.NAMESPACE)) { isIBB = true; } } if (!isByteStream && !isIBB) { final XMPPError error = new XMPPError( XMPPError.Condition.bad_request, "No acceptable transfer mechanism"); throw new XMPPException(error.getMessage(), error); } if (isByteStream && isIBB) { return new FaultTolerantNegotiator(connection, byteStreamTransferManager, inbandTransferManager); } else if (isByteStream) { return byteStreamTransferManager; } else { return inbandTransferManager; } } private FormField getStreamMethodField(DataForm form) { FormField field = null; for (final Iterator<FormField> it = form.getFields(); it.hasNext();) { field = it.next(); if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) { break; } field = null; } return field; } /** * 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.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>, * which is the preferred method of transfer, and <a * href="http://www.jabber.org/jeps/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 JEP-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 XMPPException * Thrown if there is an error negotiating the file transfer. */ public StreamNegotiator negotiateOutgoingTransfer(final String userID, final String streamID, final String fileName, final long size, final String desc, int responseTimeout) throws XMPPException { final StreamInitiation si = new StreamInitiation(); si.setSesssionID(streamID); si.setMimeType(URLConnection.guessContentTypeFromName(fileName)); final 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); final PacketCollector collector = connection .createPacketCollector(new PacketIDFilter(si.getPacketID())); connection.sendPacket(si); final Packet siResponse = collector.nextResult(responseTimeout); collector.cancel(); if (siResponse instanceof IQ) { final IQ iqResponse = (IQ) siResponse; if (iqResponse.getType().equals(IQ.Type.RESULT)) { final StreamInitiation response = (StreamInitiation) siResponse; return getOutgoingNegotiator(getStreamMethodField(response .getFeatureNegotiationForm())); } else if (iqResponse.getType().equals(IQ.Type.ERROR)) { throw new XMPPException(iqResponse.getError()); } else { throw new XMPPException("File transfer response unreadable"); } } else { return null; } } /** * Reject a stream initiation request from a remote user. * * @param si * The Stream Initiation request to reject. */ public void rejectStream(final StreamInitiation si) { final XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined"); final IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), IQ.Type.ERROR); iqPacket.setError(error); connection.sendPacket(iqPacket); } /** * 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 XMPPException * If there are either no stream methods contained in the * packet, or there is not an appropriate stream method. */ public StreamNegotiator selectStreamNegotiator(FileTransferRequest request) throws XMPPException { final StreamInitiation si = request.getStreamInitiation(); final FormField streamMethodField = getStreamMethodField(si .getFeatureNegotiationForm()); if (streamMethodField == null) { final String errorMessage = "No stream methods contained in packet."; final XMPPError error = new XMPPError( XMPPError.Condition.bad_request, errorMessage); final IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), IQ.Type.ERROR); iqPacket.setError(error); connection.sendPacket(iqPacket); throw new XMPPException(errorMessage, error); } // select the appropriate protocol StreamNegotiator selectedStreamNegotiator; try { selectedStreamNegotiator = getNegotiator(streamMethodField); } catch (final XMPPException e) { final IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), IQ.Type.ERROR); iqPacket.setError(e.getXMPPError()); connection.sendPacket(iqPacket); throw e; } // return the appropriate negotiator return selectedStreamNegotiator; } }