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.packet.DataForm; 046 047import org.jxmpp.jid.Jid; 048 049/** 050 * Manages the negotiation of file transfers according to XEP-0096. If a file is 051 * being sent the remote user chooses the type of stream under which the file 052 * will be sent. 053 * 054 * @author Alexander Wenckus 055 * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a> 056 */ 057public final class FileTransferNegotiator extends Manager { 058 059 public static final String SI_NAMESPACE = "http://jabber.org/protocol/si"; 060 public static final String SI_PROFILE_FILE_TRANSFER_NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer"; 061 private static final String[] NAMESPACE = { SI_NAMESPACE, SI_PROFILE_FILE_TRANSFER_NAMESPACE }; 062 063 private static final Map<XMPPConnection, FileTransferNegotiator> INSTANCES = new WeakHashMap<>(); 064 065 private static final String STREAM_INIT_PREFIX = "jsi_"; 066 067 protected static final String STREAM_DATA_FIELD_NAME = "stream-method"; 068 069 private static final Random randomGenerator = new Random(); 070 071 /** 072 * A static variable to use only offer IBB for file transfer. It is generally recommend to only 073 * set this variable to true for testing purposes as IBB is the backup file transfer method 074 * and shouldn't be used as the only transfer method in production systems. 075 */ 076 public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true; 077 078 /** 079 * Returns the file transfer negotiator related to a particular connection. 080 * When this class is requested on a particular connection the file transfer 081 * service is automatically enabled. 082 * 083 * @param connection The connection for which the transfer manager is desired 084 * @return The FileTransferNegotiator 085 */ 086 public static synchronized FileTransferNegotiator getInstanceFor( 087 final XMPPConnection connection) { 088 FileTransferNegotiator fileTransferNegotiator = INSTANCES.get(connection); 089 if (fileTransferNegotiator == null) { 090 fileTransferNegotiator = new FileTransferNegotiator(connection); 091 INSTANCES.put(connection, fileTransferNegotiator); 092 } 093 return fileTransferNegotiator; 094 } 095 096 /** 097 * Enable the Jabber services related to file transfer on the particular 098 * connection. 099 * 100 * @param connection The connection on which to enable or disable the services. 101 * @param isEnabled True to enable, false to disable. 102 */ 103 private static void setServiceEnabled(final XMPPConnection connection, 104 final boolean isEnabled) { 105 ServiceDiscoveryManager manager = ServiceDiscoveryManager 106 .getInstanceFor(connection); 107 108 List<String> namespaces = new ArrayList<>(); 109 namespaces.addAll(Arrays.asList(NAMESPACE)); 110 namespaces.add(DataPacketExtension.NAMESPACE); 111 if (!IBB_ONLY) { 112 namespaces.add(Bytestream.NAMESPACE); 113 } 114 115 for (String namespace : namespaces) { 116 if (isEnabled) { 117 manager.addFeature(namespace); 118 } else { 119 manager.removeFeature(namespace); 120 } 121 } 122 } 123 124 /** 125 * Checks to see if all file transfer related services are enabled on the 126 * connection. 127 * 128 * @param connection The connection to check 129 * @return True if all related services are enabled, false if they are not. 130 */ 131 public static boolean isServiceEnabled(final XMPPConnection connection) { 132 ServiceDiscoveryManager manager = ServiceDiscoveryManager 133 .getInstanceFor(connection); 134 135 List<String> namespaces = new ArrayList<>(); 136 namespaces.addAll(Arrays.asList(NAMESPACE)); 137 namespaces.add(DataPacketExtension.NAMESPACE); 138 if (!IBB_ONLY) { 139 namespaces.add(Bytestream.NAMESPACE); 140 } 141 142 for (String namespace : namespaces) { 143 if (!manager.includesFeature(namespace)) { 144 return false; 145 } 146 } 147 return true; 148 } 149 150 /** 151 * Returns a collection of the supported transfer protocols. 152 * 153 * @return Returns a collection of the supported transfer protocols. 154 */ 155 public static Collection<String> getSupportedProtocols() { 156 List<String> protocols = new ArrayList<>(); 157 protocols.add(DataPacketExtension.NAMESPACE); 158 if (!IBB_ONLY) { 159 protocols.add(Bytestream.NAMESPACE); 160 } 161 return Collections.unmodifiableList(protocols); 162 } 163 164 // non-static 165 166 private final StreamNegotiator byteStreamTransferManager; 167 168 private final StreamNegotiator inbandTransferManager; 169 170 private FileTransferNegotiator(final XMPPConnection connection) { 171 super(connection); 172 byteStreamTransferManager = new Socks5TransferNegotiator(connection); 173 inbandTransferManager = new IBBTransferNegotiator(connection); 174 175 setServiceEnabled(connection, true); 176 } 177 178 /** 179 * Selects an appropriate stream negotiator after examining the incoming file transfer request. 180 * 181 * @param request The related file transfer request. 182 * @return The file transfer object that handles the transfer 183 * @throws NoStreamMethodsOfferedException If there are either no stream methods contained in the packet, or 184 * there is not an appropriate stream method. 185 * @throws NotConnectedException 186 * @throws NoAcceptableTransferMechanisms 187 * @throws InterruptedException 188 */ 189 public StreamNegotiator selectStreamNegotiator( 190 FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException { 191 StreamInitiation si = request.getStreamInitiation(); 192 FormField streamMethodField = getStreamMethodField(si 193 .getFeatureNegotiationForm()); 194 195 if (streamMethodField == null) { 196 String errorMessage = "No stream methods contained in stanza."; 197 StanzaError.Builder error = StanzaError.from(StanzaError.Condition.bad_request, errorMessage); 198 IQ iqPacket = IQ.createErrorResponse(si, error); 199 connection().sendStanza(iqPacket); 200 throw new FileTransferException.NoStreamMethodsOfferedException(); 201 } 202 203 // select the appropriate protocol 204 StreamNegotiator selectedStreamNegotiator; 205 try { 206 selectedStreamNegotiator = getNegotiator(streamMethodField); 207 } 208 catch (NoAcceptableTransferMechanisms e) { 209 IQ iqPacket = IQ.createErrorResponse(si, StanzaError.from(StanzaError.Condition.bad_request, "No acceptable transfer mechanism")); 210 connection().sendStanza(iqPacket); 211 throw e; 212 } 213 214 // return the appropriate negotiator 215 216 return selectedStreamNegotiator; 217 } 218 219 private static FormField getStreamMethodField(DataForm form) { 220 return form.getField(STREAM_DATA_FIELD_NAME); 221 } 222 223 private StreamNegotiator getNegotiator(final FormField field) 224 throws NoAcceptableTransferMechanisms { 225 String variable; 226 boolean isByteStream = false; 227 boolean isIBB = false; 228 for (FormField.Option option : field.getOptions()) { 229 variable = option.getValue(); 230 if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) { 231 isByteStream = true; 232 } 233 else if (variable.equals(DataPacketExtension.NAMESPACE)) { 234 isIBB = true; 235 } 236 } 237 238 if (!isByteStream && !isIBB) { 239 throw new FileTransferException.NoAcceptableTransferMechanisms(); 240 } 241 242 if (isByteStream && isIBB) { 243 return new FaultTolerantNegotiator(connection(), 244 byteStreamTransferManager, 245 inbandTransferManager); 246 } 247 else 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(Math.abs(randomGenerator.nextLong())); 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 300 * @throws NoResponseException 301 * @throws NoAcceptableTransferMechanisms 302 * @throws InterruptedException 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 if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) { 346 isByteStream = true; 347 } 348 else if (variable.equals(DataPacketExtension.NAMESPACE)) { 349 isIBB = true; 350 } 351 } 352 353 if (!isByteStream && !isIBB) { 354 throw new FileTransferException.NoAcceptableTransferMechanisms(); 355 } 356 357 if (isByteStream && isIBB) { 358 return new FaultTolerantNegotiator(connection(), 359 byteStreamTransferManager, inbandTransferManager); 360 } 361 else if (isByteStream) { 362 return byteStreamTransferManager; 363 } 364 else { 365 return inbandTransferManager; 366 } 367 } 368 369 private static DataForm createDefaultInitiationForm() { 370 DataForm form = new DataForm(DataForm.Type.form); 371 FormField field = new FormField(STREAM_DATA_FIELD_NAME); 372 field.setType(FormField.Type.list_single); 373 if (!IBB_ONLY) { 374 field.addOption(new FormField.Option(Bytestream.NAMESPACE)); 375 } 376 field.addOption(new FormField.Option(DataPacketExtension.NAMESPACE)); 377 form.addField(field); 378 return form; 379 } 380}