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() 364 .setType(DataForm.Type.form); 365 ListSingleFormField.Builder fieldBuilder = FormField.listSingleBuilder(STREAM_DATA_FIELD_NAME); 366 367 if (!IBB_ONLY) { 368 fieldBuilder.addOption(Bytestream.NAMESPACE); 369 } 370 fieldBuilder.addOption(DataPacketExtension.NAMESPACE); 371 form.addField(fieldBuilder.build()); 372 return form.build(); 373 } 374}