001/**
002 *
003 * Copyright 2003-2006 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.filetransfer;
018
019import java.net.URLConnection;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026import java.util.Random;
027import java.util.WeakHashMap;
028
029import org.jivesoftware.smack.Manager;
030import org.jivesoftware.smack.SmackException.NoResponseException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.XMPPConnection;
033import org.jivesoftware.smack.XMPPException.XMPPErrorException;
034import org.jivesoftware.smack.packet.IQ;
035import org.jivesoftware.smack.packet.Stanza;
036import org.jivesoftware.smack.packet.StanzaError;
037
038import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
039import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
040import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
041import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms;
042import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException;
043import org.jivesoftware.smackx.si.packet.StreamInitiation;
044import org.jivesoftware.smackx.xdata.FormField;
045import org.jivesoftware.smackx.xdata.ListSingleFormField;
046import org.jivesoftware.smackx.xdata.packet.DataForm;
047
048import org.jxmpp.jid.Jid;
049
050/**
051 * Manages the negotiation of file transfers according to XEP-0096. If a file is
052 * being sent the remote user chooses the type of stream under which the file
053 * will be sent.
054 *
055 * @author Alexander Wenckus
056 * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
057 */
058public final class FileTransferNegotiator extends Manager {
059
060    public static final String SI_NAMESPACE = "http://jabber.org/protocol/si";
061    public static final String SI_PROFILE_FILE_TRANSFER_NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer";
062    private static final String[] NAMESPACE = { SI_NAMESPACE, SI_PROFILE_FILE_TRANSFER_NAMESPACE };
063
064    private static final Map<XMPPConnection, FileTransferNegotiator> INSTANCES = new WeakHashMap<>();
065
066    private static final String STREAM_INIT_PREFIX = "jsi_";
067
068    protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
069
070    private static final Random randomGenerator = new Random();
071
072    /**
073     * A static variable to use only offer IBB for file transfer. It is generally recommend to only
074     * set this variable to true for testing purposes as IBB is the backup file transfer method
075     * and shouldn't be used as the only transfer method in production systems.
076     */
077    public static boolean IBB_ONLY = System.getProperty("ibb") != null;//true;
078
079    /**
080     * Returns the file transfer negotiator related to a particular connection.
081     * When this class is requested on a particular connection the file transfer
082     * service is automatically enabled.
083     *
084     * @param connection The connection for which the transfer manager is desired
085     * @return The FileTransferNegotiator
086     */
087    public static synchronized FileTransferNegotiator getInstanceFor(
088            final XMPPConnection connection) {
089        FileTransferNegotiator fileTransferNegotiator = INSTANCES.get(connection);
090        if (fileTransferNegotiator == null) {
091            fileTransferNegotiator = new FileTransferNegotiator(connection);
092            INSTANCES.put(connection, fileTransferNegotiator);
093        }
094        return fileTransferNegotiator;
095    }
096
097    /**
098     * Enable the Jabber services related to file transfer on the particular
099     * connection.
100     *
101     * @param connection The connection on which to enable or disable the services.
102     * @param isEnabled  True to enable, false to disable.
103     */
104    private static void setServiceEnabled(final XMPPConnection connection,
105            final boolean isEnabled) {
106        ServiceDiscoveryManager manager = ServiceDiscoveryManager
107                .getInstanceFor(connection);
108
109        List<String> namespaces = new ArrayList<>();
110        namespaces.addAll(Arrays.asList(NAMESPACE));
111        namespaces.add(DataPacketExtension.NAMESPACE);
112        if (!IBB_ONLY) {
113            namespaces.add(Bytestream.NAMESPACE);
114        }
115
116        for (String namespace : namespaces) {
117            if (isEnabled) {
118                manager.addFeature(namespace);
119            } else {
120                manager.removeFeature(namespace);
121            }
122        }
123    }
124
125    /**
126     * Checks to see if all file transfer related services are enabled on the
127     * connection.
128     *
129     * @param connection The connection to check
130     * @return True if all related services are enabled, false if they are not.
131     */
132    public static boolean isServiceEnabled(final XMPPConnection connection) {
133        ServiceDiscoveryManager manager = ServiceDiscoveryManager
134                .getInstanceFor(connection);
135
136        List<String> namespaces = new ArrayList<>();
137        namespaces.addAll(Arrays.asList(NAMESPACE));
138        namespaces.add(DataPacketExtension.NAMESPACE);
139        if (!IBB_ONLY) {
140            namespaces.add(Bytestream.NAMESPACE);
141        }
142
143        for (String namespace : namespaces) {
144            if (!manager.includesFeature(namespace)) {
145                return false;
146            }
147        }
148        return true;
149    }
150
151    /**
152     * Returns a collection of the supported transfer protocols.
153     *
154     * @return Returns a collection of the supported transfer protocols.
155     */
156    public static Collection<String> getSupportedProtocols() {
157        List<String> protocols = new ArrayList<>();
158        protocols.add(DataPacketExtension.NAMESPACE);
159        if (!IBB_ONLY) {
160            protocols.add(Bytestream.NAMESPACE);
161        }
162        return Collections.unmodifiableList(protocols);
163    }
164
165    // non-static
166
167    private final StreamNegotiator byteStreamTransferManager;
168
169    private final StreamNegotiator inbandTransferManager;
170
171    private FileTransferNegotiator(final XMPPConnection connection) {
172        super(connection);
173        byteStreamTransferManager = new Socks5TransferNegotiator(connection);
174        inbandTransferManager = new IBBTransferNegotiator(connection);
175
176        setServiceEnabled(connection, true);
177    }
178
179    /**
180     * Selects an appropriate stream negotiator after examining the incoming file transfer request.
181     *
182     * @param request The related file transfer request.
183     * @return The file transfer object that handles the transfer
184     * @throws NoStreamMethodsOfferedException If there are either no stream methods contained in the packet, or
185     *                       there is not an appropriate stream method.
186     * @throws NotConnectedException if the XMPP connection is not connected.
187     * @throws NoAcceptableTransferMechanisms if no acceptable transfer mechanisms are available
188     * @throws InterruptedException if the calling thread was interrupted.
189     */
190    public StreamNegotiator selectStreamNegotiator(
191            FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException {
192        StreamInitiation si = request.getStreamInitiation();
193        ListSingleFormField streamMethodField = getStreamMethodField(si
194                .getFeatureNegotiationForm());
195
196        if (streamMethodField == null) {
197            String errorMessage = "No stream methods contained in stanza.";
198            StanzaError error = StanzaError.from(StanzaError.Condition.bad_request, errorMessage).build();
199            IQ iqPacket = IQ.createErrorResponse(si, error);
200            connection().sendStanza(iqPacket);
201            throw new FileTransferException.NoStreamMethodsOfferedException();
202        }
203
204        // select the appropriate protocol
205        StreamNegotiator selectedStreamNegotiator;
206        try {
207            selectedStreamNegotiator = getNegotiator(streamMethodField);
208        }
209        catch (NoAcceptableTransferMechanisms e) {
210            IQ iqPacket = IQ.createErrorResponse(si, StanzaError.from(StanzaError.Condition.bad_request, "No acceptable transfer mechanism").build());
211            connection().sendStanza(iqPacket);
212            throw e;
213        }
214
215        // return the appropriate negotiator
216
217        return selectedStreamNegotiator;
218    }
219
220    private static ListSingleFormField getStreamMethodField(DataForm form) {
221        return (ListSingleFormField) form.getField(STREAM_DATA_FIELD_NAME);
222    }
223
224    private StreamNegotiator getNegotiator(final ListSingleFormField field)
225            throws NoAcceptableTransferMechanisms {
226        String variable;
227        boolean isByteStream = false;
228        boolean isIBB = false;
229        for (FormField.Option option : field.getOptions()) {
230            variable = option.getValueString();
231            if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
232                isByteStream = true;
233            }
234            else if (variable.equals(DataPacketExtension.NAMESPACE)) {
235                isIBB = true;
236            }
237        }
238
239        if (!isByteStream && !isIBB) {
240            throw new FileTransferException.NoAcceptableTransferMechanisms();
241        }
242
243        if (isByteStream) {
244            return byteStreamTransferManager;
245        }
246        else {
247            return inbandTransferManager;
248        }
249    }
250
251    /**
252     * Returns a new, unique, stream ID to identify a file transfer.
253     *
254     * @return Returns a new, unique, stream ID to identify a file transfer.
255     */
256    public static String getNextStreamID() {
257        StringBuilder buffer = new StringBuilder();
258        buffer.append(STREAM_INIT_PREFIX);
259        buffer.append(randomGenerator.nextInt(Integer.MAX_VALUE) + randomGenerator.nextInt(Integer.MAX_VALUE));
260
261        return buffer.toString();
262    }
263
264    /**
265     * Send a request to another user to send them a file. The other user has
266     * the option of, accepting, rejecting, or not responding to a received file
267     * transfer request.
268     * <p>
269     * If they accept, the stanza will contain the other user's chosen stream
270     * type to send the file across. The two choices this implementation
271     * provides to the other user for file transfer are <a
272     * href="http://www.xmpp.org/extensions/jep-0065.html">SOCKS5 Bytestreams</a>,
273     * which is the preferred method of transfer, and <a
274     * href="http://www.xmpp.org/extensions/jep-0047.html">In-Band Bytestreams</a>,
275     * which is the fallback mechanism.
276     * </p>
277     * <p>
278     * The other user may choose to decline the file request if they do not
279     * desire the file, their client does not support XEP-0096, or if there are
280     * no acceptable means to transfer the file.
281     * </p>
282     * Finally, if the other user does not respond this method will return null
283     * after the specified timeout.
284     *
285     * @param userID          The userID of the user to whom the file will be sent.
286     * @param streamID        The unique identifier for this file transfer.
287     * @param fileName        The name of this file. Preferably it should include an
288     *                        extension as it is used to determine what type of file it is.
289     * @param size            The size, in bytes, of the file.
290     * @param desc            A description of the file.
291     * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
292     *                        user to respond. If they do not respond in time, this
293     * @return Returns the stream negotiator selected by the peer.
294     * @throws XMPPErrorException Thrown if there is an error negotiating the file transfer.
295     * @throws NotConnectedException if the XMPP connection is not connected.
296     * @throws NoResponseException if there was no response from the remote entity.
297     * @throws NoAcceptableTransferMechanisms if no acceptable transfer mechanisms are available
298     * @throws InterruptedException if the calling thread was interrupted.
299     */
300    public StreamNegotiator negotiateOutgoingTransfer(final Jid userID,
301            final String streamID, final String fileName, final long size,
302            final String desc, int responseTimeout) throws XMPPErrorException, NotConnectedException, NoResponseException, NoAcceptableTransferMechanisms, InterruptedException {
303        StreamInitiation si = new StreamInitiation();
304        si.setSessionID(streamID);
305        si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
306
307        StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
308        siFile.setDesc(desc);
309        si.setFile(siFile);
310
311        si.setFeatureNegotiationForm(createDefaultInitiationForm());
312
313        si.setFrom(connection().getUser());
314        si.setTo(userID);
315        si.setType(IQ.Type.set);
316
317        Stanza siResponse = connection().createStanzaCollectorAndSend(si).nextResultOrThrow(
318                        responseTimeout);
319
320        if (siResponse instanceof IQ) {
321            IQ iqResponse = (IQ) siResponse;
322            if (iqResponse.getType().equals(IQ.Type.result)) {
323                StreamInitiation response = (StreamInitiation) siResponse;
324                return getOutgoingNegotiator(getStreamMethodField(response
325                        .getFeatureNegotiationForm()));
326
327            }
328            else {
329                throw new XMPPErrorException(iqResponse, iqResponse.getError());
330            }
331        }
332        else {
333            return null;
334        }
335    }
336
337    private StreamNegotiator getOutgoingNegotiator(final FormField field) throws NoAcceptableTransferMechanisms {
338        boolean isByteStream = false;
339        boolean isIBB = false;
340        for (CharSequence variable : field.getValues()) {
341            String variableString = variable.toString();
342            if (variableString.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
343                isByteStream = true;
344            }
345            else if (variableString.equals(DataPacketExtension.NAMESPACE)) {
346                isIBB = true;
347            }
348        }
349
350        if (!isByteStream && !isIBB) {
351            throw new FileTransferException.NoAcceptableTransferMechanisms();
352        }
353
354        if (isByteStream) {
355            return byteStreamTransferManager;
356        }
357        else {
358            return inbandTransferManager;
359        }
360    }
361
362    private static DataForm createDefaultInitiationForm() {
363        DataForm.Builder form = DataForm.builder(DataForm.Type.form);
364        ListSingleFormField.Builder fieldBuilder = FormField.listSingleBuilder(STREAM_DATA_FIELD_NAME);
365
366        if (!IBB_ONLY) {
367            fieldBuilder.addOption(Bytestream.NAMESPACE);
368        }
369        fieldBuilder.addOption(DataPacketExtension.NAMESPACE);
370        form.addField(fieldBuilder.build());
371        return form.build();
372    }
373}