/** * * 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 XEP-0096: SI File Transfer */ public final 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 INSTANCES = new WeakHashMap<>(); 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 namespaces = new ArrayList<>(); 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 namespaces = new ArrayList<>(); 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 getSupportedProtocols() { List protocols = new ArrayList<>(); 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.Builder 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 static FormField getStreamMethodField(DataForm form) { return form.getField(STREAM_DATA_FIELD_NAME); } 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 static 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. *

* If they accept, the stanza(/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 SOCKS5 Bytestreams, * which is the preferred method of transfer, and In-Band Bytestreams, * which is the fallback mechanism. *

*

* 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. *

* 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().createStanzaCollectorAndSend(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, 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 static 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; } }