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