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