diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FaultTolerantNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FaultTolerantNegotiator.java new file mode 100644 index 000000000..dc09bc317 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FaultTolerantNegotiator.java @@ -0,0 +1,189 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.*; +import java.util.List; +import java.util.ArrayList; + + +/** + * The fault tolerant negotiator takes two stream negotiators, the primary and the secondary + * negotiator. If the primary negotiator fails during the stream negotiaton process, the second + * negotiator is used. + */ +public class FaultTolerantNegotiator extends StreamNegotiator { + + private StreamNegotiator primaryNegotiator; + private StreamNegotiator secondaryNegotiator; + private XMPPConnection connection; + private PacketFilter primaryFilter; + private PacketFilter secondaryFilter; + + public FaultTolerantNegotiator(XMPPConnection connection, StreamNegotiator primary, + StreamNegotiator secondary) { + this.primaryNegotiator = primary; + this.secondaryNegotiator = secondary; + this.connection = connection; + } + + public PacketFilter getInitiationPacketFilter(String from, String streamID) { + if (primaryFilter == null || secondaryFilter == null) { + primaryFilter = primaryNegotiator.getInitiationPacketFilter(from, streamID); + secondaryFilter = secondaryNegotiator.getInitiationPacketFilter(from, streamID); + } + return new OrFilter(primaryFilter, secondaryFilter); + } + + InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException { + throw new UnsupportedOperationException("Negotiation only handled by create incoming " + + "stream method."); + } + + final Packet initiateIncomingStream(XMPPConnection connection, StreamInitiation initiation) { + throw new UnsupportedOperationException("Initiation handled by createIncomingStream " + + "method"); + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + PacketCollector collector = connection.createPacketCollector( + getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID())); + + System.out.println(new java.util.Date().toString() + ": FaultTolerantNegotiator>createIncomingStream:80"); + connection.sendPacket(super.createInitiationAccept(initiation, getNamespaces())); + System.out.println(new java.util.Date().toString() + ": FaultTolerantNegotiator>createIncomingStream:82"); + + CompletionService service + = new ExecutorCompletionService(Executors.newFixedThreadPool(2)); + List> futures = new ArrayList>(); + InputStream stream = null; + XMPPException exception = null; + try { + futures.add(service.submit(new NegotiatorService(collector))); + futures.add(service.submit(new NegotiatorService(collector))); + System.out.println(new java.util.Date().toString() + ": FaultTolerantNegotiator>createIncomingStream:92"); + + int i = 0; + while (stream == null && i < futures.size()) { + Future future; + try { + i++; + System.out.println(new java.util.Date().toString() + ": FaultTolerantNegotiator>createIncomingStream:99"); + future = service.poll(10, TimeUnit.SECONDS); + System.out.println(new java.util.Date().toString() + ": FaultTolerantNegotiator>createIncomingStream:101"); + } + catch (InterruptedException e) { + continue; + } + + if (future == null) { + continue; + } + + try { + stream = future.get(); + } + catch (InterruptedException e) { + /* Do Nothing */ + } + catch (ExecutionException e) { + exception = new XMPPException(e.getCause()); + } + } + } + finally { + for (Future future : futures) { + future.cancel(true); + } + collector.cancel(); + } + if (stream == null) { + if (exception != null) { + throw exception; + } + else { + throw new XMPPException("File transfer negotiation failed."); + } + } + + return stream; + } + + private StreamNegotiator determineNegotiator(Packet streamInitiation) { + return primaryFilter.accept(streamInitiation) ? primaryNegotiator : secondaryNegotiator; + } + + public OutputStream createOutgoingStream(String streamID, String initiator, String target) + throws XMPPException { + OutputStream stream; + try { + stream = primaryNegotiator.createOutgoingStream(streamID, initiator, target); + } + catch (XMPPException ex) { + stream = secondaryNegotiator.createOutgoingStream(streamID, initiator, target); + } + + return stream; + } + + public String[] getNamespaces() { + String[] primary = primaryNegotiator.getNamespaces(); + String[] secondary = secondaryNegotiator.getNamespaces(); + + String[] namespaces = new String[primary.length + secondary.length]; + System.arraycopy(primary, 0, namespaces, 0, primary.length); + System.arraycopy(secondary, 0, namespaces, primary.length, secondary.length); + + return namespaces; + } + + public void cleanup() { + } + + private class NegotiatorService implements Callable { + + private PacketCollector collector; + + NegotiatorService(PacketCollector collector) { + this.collector = collector; + } + + public InputStream call() throws Exception { + Packet streamInitiation = collector.nextResult( + SmackConfiguration.getPacketReplyTimeout() * 2); + if (streamInitiation == null) { + throw new XMPPException("No response from remote client"); + } + StreamNegotiator negotiator = determineNegotiator(streamInitiation); + return negotiator.negotiateIncomingStream(streamInitiation); + } + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransfer.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransfer.java new file mode 100644 index 000000000..9954adf4e --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransfer.java @@ -0,0 +1,376 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.jivesoftware.smack.XMPPException; + +/** + * Contains the generic file information and progress related to a particular + * file transfer. + * + * @author Alexander Wenckus + * + */ +public abstract class FileTransfer { + + private String fileName; + + private String filePath; + + private long fileSize; + + private String peer; + + private Status status = Status.initial; + + private final Object statusMonitor = new Object(); + + protected FileTransferNegotiator negotiator; + + protected String streamID; + + protected long amountWritten = -1; + + private Error error; + + private Exception exception; + + /** + * Buffer size between input and output + */ + private static final int BUFFER_SIZE = 8192; + + protected FileTransfer(String peer, String streamID, + FileTransferNegotiator negotiator) { + this.peer = peer; + this.streamID = streamID; + this.negotiator = negotiator; + } + + protected void setFileInfo(String fileName, long fileSize) { + this.fileName = fileName; + this.fileSize = fileSize; + } + + protected void setFileInfo(String path, String fileName, long fileSize) { + this.filePath = path; + this.fileName = fileName; + this.fileSize = fileSize; + } + + /** + * Returns the size of the file being transfered. + * + * @return Returns the size of the file being transfered. + */ + public long getFileSize() { + return fileSize; + } + + /** + * Returns the name of the file being transfered. + * + * @return Returns the name of the file being transfered. + */ + public String getFileName() { + return fileName; + } + + /** + * Returns the local path of the file. + * + * @return Returns the local path of the file. + */ + public String getFilePath() { + return filePath; + } + + /** + * Returns the JID of the peer for this file transfer. + * + * @return Returns the JID of the peer for this file transfer. + */ + public String getPeer() { + return peer; + } + + /** + * Returns the progress of the file transfer as a number between 0 and 1. + * + * @return Returns the progress of the file transfer as a number between 0 + * and 1. + */ + public double getProgress() { + if (amountWritten <= 0 || fileSize <= 0) { + return 0; + } + return (double) amountWritten / (double) fileSize; + } + + /** + * Returns true if the transfer has been cancelled, if it has stopped because + * of a an error, or the transfer completed succesfully. + * + * @return Returns true if the transfer has been cancelled, if it has stopped + * because of a an error, or the transfer completed succesfully. + */ + public boolean isDone() { + return status == Status.cancelled || status == Status.error + || status == Status.complete || status == Status.refused; + } + + /** + * Retuns the current status of the file transfer. + * + * @return Retuns the current status of the file transfer. + */ + public Status getStatus() { + return status; + } + + protected void setError(Error type) { + this.error = type; + } + + /** + * When {@link #getStatus()} returns that there was an {@link Status#error} + * during the transfer, the type of error can be retrieved through this + * method. + * + * @return Returns the type of error that occured if one has occured. + */ + public Error getError() { + return error; + } + + /** + * If an exception occurs asynchronously it will be stored for later + * retrival. If there is an error there maybe an exception set. + * + * @return The exception that occured or null if there was no exception. + * @see #getError() + */ + public Exception getException() { + return exception; + } + + /** + * Cancels the file transfer. + */ + public abstract void cancel(); + + protected void setException(Exception exception) { + this.exception = exception; + } + + protected void setStatus(Status status) { + synchronized (statusMonitor) { + this.status = status; + } + } + + protected boolean updateStatus(Status oldStatus, Status newStatus) { + synchronized (statusMonitor) { + if (oldStatus != status) { + return false; + } + status = newStatus; + return true; + } + } + + protected void writeToStream(final InputStream in, final OutputStream out) + throws XMPPException + { + final byte[] b = new byte[BUFFER_SIZE]; + int count = 0; + amountWritten = 0; + + do { + // write to the output stream + try { + out.write(b, 0, count); + } catch (IOException e) { + throw new XMPPException("error writing to output stream", e); + } + + amountWritten += count; + + // read more bytes from the input stream + try { + count = in.read(b); + } catch (IOException e) { + throw new XMPPException("error reading from input stream", e); + } + } while (count != -1 && !getStatus().equals(Status.cancelled)); + + // the connection was likely terminated abrubtly if these are not equal + if (!getStatus().equals(Status.cancelled) && getError() == Error.none + && amountWritten != fileSize) { + setStatus(Status.error); + this.error = Error.connection; + } + } + + /** + * A class to represent the current status of the file transfer. + * + * @author Alexander Wenckus + * + */ + public enum Status { + + /** + * An error occured during the transfer. + * + * @see FileTransfer#getError() + */ + error("Error"), + + /** + * The initial status of the file transfer. + */ + initial("Initial"), + + /** + * The file transfer is being negotiated with the peer. The party + * recieving the file has the option to accept or refuse a file transfer + * request. If they accept, then the process of stream negotiation will + * begin. If they refuse the file will not be transfered. + * + * @see #negotiating_stream + */ + negotiating_transfer("Negotiating Transfer"), + + /** + * The peer has refused the file transfer request halting the file + * transfer negotiation process. + */ + refused("Refused"), + + /** + * The stream to transfer the file is being negotiated over the chosen + * stream type. After the stream negotiating process is complete the + * status becomes negotiated. + * + * @see #negotiated + */ + negotiating_stream("Negotiating Stream"), + + /** + * After the stream negotitation has completed the intermediate state + * between the time when the negotiation is finished and the actual + * transfer begins. + */ + negotiated("Negotiated"), + + /** + * The transfer is in progress. + * + * @see FileTransfer#getProgress() + */ + in_progress("In Progress"), + + /** + * The transfer has completed successfully. + */ + complete("Complete"), + + /** + * The file transfer was canceled + */ + cancelled("Cancelled"); + + private String status; + + private Status(String status) { + this.status = status; + } + + public String toString() { + return status; + } + } + + /** + * Return the length of bytes written out to the stream. + * @return the amount in bytes written out. + */ + public long getAmountWritten(){ + return amountWritten; + } + + public enum Error { + /** + * No error + */ + none("No error"), + + /** + * The peer did not find any of the provided stream mechanisms + * acceptable. + */ + not_acceptable("The peer did not find any of the provided stream mechanisms acceptable."), + + /** + * The provided file to transfer does not exist or could not be read. + */ + bad_file("The provided file to transfer does not exist or could not be read."), + + /** + * The remote user did not respond or the connection timed out. + */ + no_response("The remote user did not respond or the connection timed out."), + + /** + * An error occured over the socket connected to send the file. + */ + connection("An error occured over the socket connected to send the file."), + + /** + * An error occured while sending or recieving the file + */ + stream("An error occured while sending or recieving the file."); + + private final String msg; + + private Error(String msg) { + this.msg = msg; + } + + /** + * Returns a String representation of this error. + * + * @return Returns a String representation of this error. + */ + public String getMessage() { + return msg; + } + + public String toString() { + return msg; + } + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferListener.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferListener.java new file mode 100644 index 000000000..69b5c105d --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferListener.java @@ -0,0 +1,36 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +/** + * File transfers can cause several events to be raised. These events can be + * monitored through this interface. + * + * @author Alexander Wenckus + */ +public interface FileTransferListener { + /** + * A request to send a file has been recieved from another user. + * + * @param request + * The request from the other user. + */ + public void fileTransferRequest(final FileTransferRequest request); +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferManager.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferManager.java new file mode 100644 index 000000000..8294fbd6f --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferManager.java @@ -0,0 +1,178 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * The file transfer manager class handles the sending and recieving of files. + * To send a file invoke the {@link #createOutgoingFileTransfer(String)} method. + *

+ * And to recieve a file add a file transfer listener to the manager. The + * listener will notify you when there is a new file transfer request. To create + * the {@link IncomingFileTransfer} object accept the transfer, or, if the + * transfer is not desirable reject it. + * + * @author Alexander Wenckus + * + */ +public class FileTransferManager { + + private final FileTransferNegotiator fileTransferNegotiator; + + private List listeners; + + private XMPPConnection connection; + + /** + * Creates a file transfer manager to initiate and receive file transfers. + * + * @param connection + * The XMPPConnection that the file transfers will use. + */ + public FileTransferManager(XMPPConnection connection) { + this.connection = connection; + this.fileTransferNegotiator = FileTransferNegotiator + .getInstanceFor(connection); + } + + /** + * Add a file transfer listener to listen to incoming file transfer + * requests. + * + * @param li + * The listener + * @see #removeFileTransferListener(FileTransferListener) + * @see FileTransferListener + */ + public void addFileTransferListener(final FileTransferListener li) { + if (listeners == null) { + initListeners(); + } + synchronized (this.listeners) { + listeners.add(li); + } + } + + private void initListeners() { + listeners = new ArrayList(); + + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + fireNewRequest((StreamInitiation) packet); + } + }, new AndFilter(new PacketTypeFilter(StreamInitiation.class), + new IQTypeFilter(IQ.Type.SET))); + } + + protected void fireNewRequest(StreamInitiation initiation) { + FileTransferListener[] listeners = null; + synchronized (this.listeners) { + listeners = new FileTransferListener[this.listeners.size()]; + this.listeners.toArray(listeners); + } + FileTransferRequest request = new FileTransferRequest(this, initiation); + for (int i = 0; i < listeners.length; i++) { + listeners[i].fileTransferRequest(request); + } + } + + /** + * Removes a file transfer listener. + * + * @param li + * The file transfer listener to be removed + * @see FileTransferListener + */ + public void removeFileTransferListener(final FileTransferListener li) { + if (listeners == null) { + return; + } + synchronized (this.listeners) { + listeners.remove(li); + } + } + + /** + * Creates an OutgoingFileTransfer to send a file to another user. + * + * @param userID + * The fully qualified jabber ID with resource of the user to + * send the file to. + * @return The send file object on which the negotiated transfer can be run. + */ + public OutgoingFileTransfer createOutgoingFileTransfer(String userID) { + if (userID == null || StringUtils.parseName(userID).length() <= 0 + || StringUtils.parseServer(userID).length() <= 0 + || StringUtils.parseResource(userID).length() <= 0) { + throw new IllegalArgumentException( + "The provided user id was not fully qualified"); + } + + return new OutgoingFileTransfer(connection.getUser(), userID, + fileTransferNegotiator.getNextStreamID(), + fileTransferNegotiator); + } + + /** + * When the file transfer request is acceptable, this method should be + * invoked. It will create an IncomingFileTransfer which allows the + * transmission of the file to procede. + * + * @param request + * The remote request that is being accepted. + * @return The IncomingFileTransfer which manages the download of the file + * from the transfer initiator. + */ + protected IncomingFileTransfer createIncomingFileTransfer( + FileTransferRequest request) { + if (request == null) { + throw new NullPointerException("RecieveRequest cannot be null"); + } + + IncomingFileTransfer transfer = new IncomingFileTransfer(request, + fileTransferNegotiator); + transfer.setFileInfo(request.getFileName(), request.getFileSize()); + + return transfer; + } + + protected void rejectIncomingFileTransfer(FileTransferRequest request) { + StreamInitiation initiation = request.getStreamInitiation(); + + IQ rejection = FileTransferNegotiator.createIQ( + initiation.getPacketID(), initiation.getFrom(), initiation + .getTo(), IQ.Type.ERROR); + rejection.setError(new XMPPError(XMPPError.Condition.forbidden)); + connection.sendPacket(rejection); + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferNegotiator.java new file mode 100644 index 000000000..1d2354b8e --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferNegotiator.java @@ -0,0 +1,462 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.net.URLConnection; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages the negotiation of file transfers according to JEP-0096. If a file is + * being sent the remote user chooses the type of stream under which the file + * will be sent. + * + * @author Alexander Wenckus + * @see JEP-0096: File Transfer + */ +public class FileTransferNegotiator { + + // Static + + /** + * The XMPP namespace of the SOCKS5 bytestream + */ + public static final String BYTE_STREAM = "http://jabber.org/protocol/bytestreams"; + + /** + * The XMPP namespace of the In-Band bytestream + */ + public static final String INBAND_BYTE_STREAM = "http://jabber.org/protocol/ibb"; + + private static final String[] NAMESPACE = { + "http://jabber.org/protocol/si/profile/file-transfer", + "http://jabber.org/protocol/si", BYTE_STREAM, INBAND_BYTE_STREAM}; + + private static final String[] PROTOCOLS = {BYTE_STREAM, INBAND_BYTE_STREAM}; + + private static final Map transferObject = + new ConcurrentHashMap(); + + private static final String STREAM_INIT_PREFIX = "jsi_"; + + protected static final String STREAM_DATA_FIELD_NAME = "stream-method"; + + private static final Random randomGenerator = new Random(); + + /** + * A static variable to use only offer IBB for file transfer. It is generally recommend to only + * set this variable to true for testing purposes as IBB is the backup file transfer method + * and shouldn't be used as the only transfer method in production systems. + */ + public static boolean IBB_ONLY = false; + + /** + * Returns the file transfer negotiator related to a particular connection. + * When this class is requested on a particular connection the file transfer + * service is automatically enabled. + * + * @param connection The connection for which the transfer manager is desired + * @return The IMFileTransferManager + */ + public static FileTransferNegotiator getInstanceFor( + final XMPPConnection connection) { + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + if (!connection.isConnected()) { + return null; + } + + if (transferObject.containsKey(connection)) { + return transferObject.get(connection); + } + else { + FileTransferNegotiator transfer = new FileTransferNegotiator( + connection); + setServiceEnabled(connection, true); + transferObject.put(connection, transfer); + return transfer; + } + } + + /** + * Enable the Jabber services related to file transfer on the particular + * connection. + * + * @param connection The connection on which to enable or disable the services. + * @param isEnabled True to enable, false to disable. + */ + public static void setServiceEnabled(final XMPPConnection connection, + final boolean isEnabled) { + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + for (String ns : NAMESPACE) { + if (isEnabled) { + manager.addFeature(ns); + } + else { + manager.removeFeature(ns); + } + } + } + + /** + * Checks to see if all file transfer related services are enabled on the + * connection. + * + * @param connection The connection to check + * @return True if all related services are enabled, false if they are not. + */ + public static boolean isServiceEnabled(final XMPPConnection connection) { + for (String ns : NAMESPACE) { + if (!ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(ns)) + return false; + } + return true; + } + + /** + * A convience method to create an IQ packet. + * + * @param ID The packet ID of the + * @param to To whom the packet is addressed. + * @param from From whom the packet is sent. + * @param type The iq type of the packet. + * @return The created IQ packet. + */ + public static IQ createIQ(final String ID, final String to, + final String from, final IQ.Type type) { + IQ iqPacket = new IQ() { + public String getChildElementXML() { + return null; + } + }; + iqPacket.setPacketID(ID); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + + return iqPacket; + } + + /** + * Returns a collection of the supported transfer protocols. + * + * @return Returns a collection of the supported transfer protocols. + */ + public static Collection getSupportedProtocols() { + return Collections.unmodifiableList(Arrays.asList(PROTOCOLS)); + } + + // non-static + + private final XMPPConnection connection; + + private final Socks5TransferNegotiatorManager byteStreamTransferManager; + + private final StreamNegotiator inbandTransferManager; + + private FileTransferNegotiator(final XMPPConnection connection) { + configureConnection(connection); + + this.connection = connection; + byteStreamTransferManager = new Socks5TransferNegotiatorManager(connection); + inbandTransferManager = new IBBTransferNegotiator(connection); + } + + private void configureConnection(final XMPPConnection connection) { + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + cleanup(connection); + } + + public void connectionClosedOnError(Exception e) { + cleanup(connection); + } + + public void reconnectionFailed(Exception e) { + // ignore + } + + public void reconnectionSuccessful() { + // ignore + } + + public void reconnectingIn(int seconds) { + // ignore + } + }); + } + + private void cleanup(final XMPPConnection connection) { + if (transferObject.remove(connection) != null) { + byteStreamTransferManager.cleanup(); + inbandTransferManager.cleanup(); + } + } + + /** + * Selects an appropriate stream negotiator after examining the incoming file transfer request. + * + * @param request The related file transfer request. + * @return The file transfer object that handles the transfer + * @throws XMPPException If there are either no stream methods contained in the packet, or + * there is not an appropriate stream method. + */ + public StreamNegotiator selectStreamNegotiator( + FileTransferRequest request) throws XMPPException { + StreamInitiation si = request.getStreamInitiation(); + FormField streamMethodField = getStreamMethodField(si + .getFeatureNegotiationForm()); + + if (streamMethodField == null) { + String errorMessage = "No stream methods contained in packet."; + XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage); + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(error); + connection.sendPacket(iqPacket); + throw new XMPPException(errorMessage, error); + } + + // select the appropriate protocol + + StreamNegotiator selectedStreamNegotiator; + try { + selectedStreamNegotiator = getNegotiator(streamMethodField); + } + catch (XMPPException e) { + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(e.getXMPPError()); + connection.sendPacket(iqPacket); + throw e; + } + + // return the appropriate negotiator + + return selectedStreamNegotiator; + } + + private FormField getStreamMethodField(DataForm form) { + FormField field = null; + for (Iterator it = form.getFields(); it.hasNext();) { + field = (FormField) it.next(); + if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) { + break; + } + field = null; + } + return field; + } + + private StreamNegotiator getNegotiator(final FormField field) + throws XMPPException { + String variable; + boolean isByteStream = false; + boolean isIBB = false; + for (Iterator it = field.getOptions(); it.hasNext();) { + variable = ((FormField.Option) it.next()).getValue(); + if (variable.equals(BYTE_STREAM) && !IBB_ONLY) { + isByteStream = true; + } + else if (variable.equals(INBAND_BYTE_STREAM)) { + isIBB = true; + } + } + + if (!isByteStream && !isIBB) { + XMPPError error = new XMPPError(XMPPError.Condition.bad_request, + "No acceptable transfer mechanism"); + throw new XMPPException(error.getMessage(), error); + } + + if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) { + return new FaultTolerantNegotiator(connection, + byteStreamTransferManager.createNegotiator(), + inbandTransferManager); + } + else if (isByteStream) { + return byteStreamTransferManager.createNegotiator(); + } + else { + return inbandTransferManager; + } + } + + /** + * Reject a stream initiation request from a remote user. + * + * @param si The Stream Initiation request to reject. + */ + public void rejectStream(final StreamInitiation si) { + XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined"); + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(error); + connection.sendPacket(iqPacket); + } + + /** + * Returns a new, unique, stream ID to identify a file transfer. + * + * @return Returns a new, unique, stream ID to identify a file transfer. + */ + public String getNextStreamID() { + StringBuilder buffer = new StringBuilder(); + buffer.append(STREAM_INIT_PREFIX); + buffer.append(Math.abs(randomGenerator.nextLong())); + + return buffer.toString(); + } + + /** + * Send a request to another user to send them a file. The other user has + * the option of, accepting, rejecting, or not responding to a received file + * transfer request. + *

+ * If they accept, the packet will contain the other user's choosen stream + * type to send the file across. The two choices this implementation + * provides to the other user for file transfer are SOCKS5 Bytestreams, + * which is the prefered method of transfer, and In-Band Bytestreams, + * which is the fallback mechanism. + *

+ * The other user may choose to decline the file request if they do not + * desire the file, their client does not support JEP-0096, or if there are + * no acceptable means to transfer the file. + *

+ * Finally, if the other user does not respond this method will return null + * after the specified timeout. + * + * @param userID The userID of the user to whom the file will be sent. + * @param streamID The unique identifier for this file transfer. + * @param fileName The name of this file. Preferably it should include an + * extension as it is used to determine what type of file it is. + * @param size The size, in bytes, of the file. + * @param desc A description of the file. + * @param responseTimeout The amount of time, in milliseconds, to wait for the remote + * user to respond. If they do not respond in time, this + * @return Returns the stream negotiator selected by the peer. + * @throws XMPPException Thrown if there is an error negotiating the file transfer. + */ + public StreamNegotiator negotiateOutgoingTransfer(final String userID, + final String streamID, final String fileName, final long size, + final String desc, int responseTimeout) throws XMPPException { + StreamInitiation si = new StreamInitiation(); + si.setSesssionID(streamID); + si.setMimeType(URLConnection.guessContentTypeFromName(fileName)); + + StreamInitiation.File siFile = new StreamInitiation.File(fileName, size); + siFile.setDesc(desc); + si.setFile(siFile); + + si.setFeatureNegotiationForm(createDefaultInitiationForm()); + + si.setFrom(connection.getUser()); + si.setTo(userID); + si.setType(IQ.Type.SET); + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(si.getPacketID())); + connection.sendPacket(si); + Packet siResponse = collector.nextResult(responseTimeout); + collector.cancel(); + + if (siResponse instanceof IQ) { + IQ iqResponse = (IQ) siResponse; + if (iqResponse.getType().equals(IQ.Type.RESULT)) { + StreamInitiation response = (StreamInitiation) siResponse; + return getOutgoingNegotiator(getStreamMethodField(response + .getFeatureNegotiationForm())); + + } + else if (iqResponse.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(iqResponse.getError()); + } + else { + throw new XMPPException("File transfer response unreadable"); + } + } + else { + return null; + } + } + + private StreamNegotiator getOutgoingNegotiator(final FormField field) + throws XMPPException { + String variable; + boolean isByteStream = false; + boolean isIBB = false; + for (Iterator it = field.getValues(); it.hasNext();) { + variable = it.next(); + if (variable.equals(BYTE_STREAM) && !IBB_ONLY) { + isByteStream = true; + } + else if (variable.equals(INBAND_BYTE_STREAM)) { + isIBB = true; + } + } + + if (!isByteStream && !isIBB) { + XMPPError error = new XMPPError(XMPPError.Condition.bad_request, + "No acceptable transfer mechanism"); + throw new XMPPException(error.getMessage(), error); + } + + if (isByteStream && isIBB) { + return new FaultTolerantNegotiator(connection, + byteStreamTransferManager.createNegotiator(), inbandTransferManager); + } + else if (isByteStream) { + return byteStreamTransferManager.createNegotiator(); + } + else { + return inbandTransferManager; + } + } + + private DataForm createDefaultInitiationForm() { + DataForm form = new DataForm(Form.TYPE_FORM); + FormField field = new FormField(STREAM_DATA_FIELD_NAME); + field.setType(FormField.TYPE_LIST_MULTI); + if (!IBB_ONLY) { + field.addOption(new FormField.Option(BYTE_STREAM)); + } + field.addOption(new FormField.Option(INBAND_BYTE_STREAM)); + form.addField(field); + return form; + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferNegotiatorManager.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferNegotiatorManager.java new file mode 100644 index 000000000..e63c9ebf6 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferNegotiatorManager.java @@ -0,0 +1,26 @@ +/** + * $Revision:$ + * $Date:$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +/** + * + */ +public interface FileTransferNegotiatorManager { + StreamNegotiator createNegotiator(); +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferRequest.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferRequest.java new file mode 100644 index 000000000..d202dca62 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/FileTransferRequest.java @@ -0,0 +1,138 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * A request to send a file recieved from another user. + * + * @author Alexander Wenckus + * + */ +public class FileTransferRequest { + private final StreamInitiation streamInitiation; + + private final FileTransferManager manager; + + /** + * A recieve request is constructed from the Stream Initiation request + * received from the initator. + * + * @param manager + * The manager handling this file transfer + * + * @param si + * The Stream initiaton recieved from the initiator. + */ + public FileTransferRequest(FileTransferManager manager, StreamInitiation si) { + this.streamInitiation = si; + this.manager = manager; + } + + /** + * Returns the name of the file. + * + * @return Returns the name of the file. + */ + public String getFileName() { + return streamInitiation.getFile().getName(); + } + + /** + * Returns the size in bytes of the file. + * + * @return Returns the size in bytes of the file. + */ + public long getFileSize() { + return streamInitiation.getFile().getSize(); + } + + /** + * Returns the description of the file provided by the requestor. + * + * @return Returns the description of the file provided by the requestor. + */ + public String getDescription() { + return streamInitiation.getFile().getDesc(); + } + + /** + * Returns the mime-type of the file. + * + * @return Returns the mime-type of the file. + */ + public String getMimeType() { + return streamInitiation.getMimeType(); + } + + /** + * Returns the fully-qualified jabber ID of the user that requested this + * file transfer. + * + * @return Returns the fully-qualified jabber ID of the user that requested + * this file transfer. + */ + public String getRequestor() { + return streamInitiation.getFrom(); + } + + /** + * Returns the stream ID that uniquely identifies this file transfer. + * + * @return Returns the stream ID that uniquely identifies this file + * transfer. + */ + public String getStreamID() { + return streamInitiation.getSessionID(); + } + + /** + * Returns the stream initiation packet that was sent by the requestor which + * contains the parameters of the file transfer being transfer and also the + * methods available to transfer the file. + * + * @return Returns the stream initiation packet that was sent by the + * requestor which contains the parameters of the file transfer + * being transfer and also the methods available to transfer the + * file. + */ + protected StreamInitiation getStreamInitiation() { + return streamInitiation; + } + + /** + * Accepts this file transfer and creates the incoming file transfer. + * + * @return Returns the IncomingFileTransfer on which the + * file transfer can be carried out. + */ + public IncomingFileTransfer accept() { + return manager.createIncomingFileTransfer(this); + } + + /** + * Rejects the file transfer request. + */ + public void reject() { + manager.rejectIncomingFileTransfer(this); + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/IBBTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/IBBTransferNegotiator.java new file mode 100644 index 000000000..19fc23368 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/IBBTransferNegotiator.java @@ -0,0 +1,459 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.packet.IBBExtensions; +import org.jivesoftware.smackx.packet.IBBExtensions.Open; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The in-band bytestream file transfer method, or IBB for short, transfers the + * file over the same XML Stream used by XMPP. It is the fall-back mechanism in + * case the SOCKS5 bytestream method of transfering files is not available. + * + * @author Alexander Wenckus + * @see JEP-0047: In-Band + * Bytestreams (IBB) + */ +public class IBBTransferNegotiator extends StreamNegotiator { + + protected static final String NAMESPACE = "http://jabber.org/protocol/ibb"; + + public static final int DEFAULT_BLOCK_SIZE = 4096; + + private XMPPConnection connection; + + /** + * The default constructor for the In-Band Bystream Negotiator. + * + * @param connection The connection which this negotiator works on. + */ + protected IBBTransferNegotiator(XMPPConnection connection) { + this.connection = connection; + } + + public PacketFilter getInitiationPacketFilter(String from, String streamID) { + return new AndFilter(new FromContainsFilter( + from), new IBBOpenSidFilter(streamID)); + } + + InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException { + Open openRequest = (Open) streamInitiation; + + if (openRequest.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(openRequest.getError()); + } + + PacketFilter dataFilter = new IBBMessageSidFilter(openRequest.getFrom(), + openRequest.getSessionID()); + PacketFilter closeFilter = new AndFilter(new PacketTypeFilter( + IBBExtensions.Close.class), new FromMatchesFilter(openRequest + .getFrom())); + + InputStream stream = new IBBInputStream(openRequest.getSessionID(), + dataFilter, closeFilter); + + initInBandTransfer(openRequest); + + return stream; + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + Packet openRequest = initiateIncomingStream(connection, initiation); + return negotiateIncomingStream(openRequest); + } + + /** + * Creates and sends the response for the open request. + * + * @param openRequest The open request recieved from the peer. + */ + private void initInBandTransfer(final Open openRequest) { + connection.sendPacket(FileTransferNegotiator.createIQ(openRequest + .getPacketID(), openRequest.getFrom(), openRequest.getTo(), + IQ.Type.RESULT)); + } + + public OutputStream createOutgoingStream(String streamID, String initiator, + String target) throws XMPPException { + Open openIQ = new Open(streamID, DEFAULT_BLOCK_SIZE); + openIQ.setTo(target); + openIQ.setType(IQ.Type.SET); + + // wait for the result from the peer + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(openIQ.getPacketID())); + connection.sendPacket(openIQ); + // We don't want to wait forever for the result + IQ openResponse = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + + if (openResponse == null) { + throw new XMPPException("No response from peer on IBB open"); + } + + IQ.Type type = openResponse.getType(); + if (!type.equals(IQ.Type.RESULT)) { + if (type.equals(IQ.Type.ERROR)) { + throw new XMPPException("Target returned an error", + openResponse.getError()); + } + else { + throw new XMPPException("Target returned unknown response"); + } + } + + return new IBBOutputStream(target, streamID, DEFAULT_BLOCK_SIZE); + } + + public String[] getNamespaces() { + return new String[]{NAMESPACE}; + } + + public void cleanup() { + } + + private class IBBOutputStream extends OutputStream { + + protected byte[] buffer; + + protected int count = 0; + + protected int seq = 0; + + final String userID; + + final private IQ closePacket; + + private String messageID; + private String sid; + + IBBOutputStream(String userID, String sid, int blockSize) { + if (blockSize <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + buffer = new byte[blockSize]; + this.userID = userID; + + Message template = new Message(userID); + messageID = template.getPacketID(); + this.sid = sid; + closePacket = createClosePacket(userID, sid); + } + + private IQ createClosePacket(String userID, String sid) { + IQ packet = new IBBExtensions.Close(sid); + packet.setTo(userID); + packet.setType(IQ.Type.SET); + return packet; + } + + public void write(int b) throws IOException { + if (count >= buffer.length) { + flushBuffer(); + } + + buffer[count++] = (byte) b; + } + + public synchronized void write(byte b[], int off, int len) + throws IOException { + if (len >= buffer.length) { + // "byte" off the first chunck to write out + writeOut(b, off, buffer.length); + // recursivly call this method again with the lesser amount subtracted. + write(b, off + buffer.length, len - buffer.length); + } else { + writeOut(b, off, len); + } + } + + private void writeOut(byte b[], int off, int len) { + if (len > buffer.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buffer, count, len); + count += len; + } + + private void flushBuffer() { + writeToXML(buffer, 0, count); + + count = 0; + } + + private synchronized void writeToXML(byte[] buffer, int offset, int len) { + Message template = createTemplate(messageID + "_" + seq); + IBBExtensions.Data ext = new IBBExtensions.Data(sid); + template.addExtension(ext); + + String enc = StringUtils.encodeBase64(buffer, offset, len, false); + + ext.setData(enc); + ext.setSeq(seq); + synchronized (this) { + try { + this.wait(100); + } + catch (InterruptedException e) { + /* Do Nothing */ + } + } + + connection.sendPacket(template); + + seq = (seq + 1 == 65535 ? 0 : seq + 1); + } + + public void close() throws IOException { + connection.sendPacket(closePacket); + } + + public void flush() throws IOException { + flushBuffer(); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public Message createTemplate(String messageID) { + Message template = new Message(userID); + template.setPacketID(messageID); + return template; + } + } + + private class IBBInputStream extends InputStream implements PacketListener { + + private String streamID; + + private PacketCollector dataCollector; + + private byte[] buffer; + + private int bufferPointer; + + private int seq = -1; + + private boolean isDone; + + private boolean isEOF; + + private boolean isClosed; + + private IQ closeConfirmation; + + private Message lastMess; + + private IBBInputStream(String streamID, PacketFilter dataFilter, + PacketFilter closeFilter) { + this.streamID = streamID; + this.dataCollector = connection.createPacketCollector(dataFilter); + connection.addPacketListener(this, closeFilter); + this.bufferPointer = -1; + } + + public synchronized int read() throws IOException { + if (isEOF || isClosed) { + return -1; + } + if (bufferPointer == -1 || bufferPointer >= buffer.length) { + loadBufferWait(); + } + + return (int) buffer[bufferPointer++]; + } + + public synchronized int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public synchronized int read(byte[] b, int off, int len) + throws IOException { + if (isEOF || isClosed) { + return -1; + } + if (bufferPointer == -1 || bufferPointer >= buffer.length) { + if (!loadBufferWait()) { + isEOF = true; + return -1; + } + } + + if (len - off > buffer.length - bufferPointer) { + len = buffer.length - bufferPointer; + } + + System.arraycopy(buffer, bufferPointer, b, off, len); + bufferPointer += len; + return len; + } + + private boolean loadBufferWait() throws IOException { + IBBExtensions.Data data; + + Message mess = null; + while (mess == null) { + if (isDone) { + mess = (Message) dataCollector.pollResult(); + if (mess == null) { + return false; + } + } + else { + mess = (Message) dataCollector.nextResult(1000); + } + } + lastMess = mess; + data = (IBBExtensions.Data) mess.getExtension( + IBBExtensions.Data.ELEMENT_NAME, + IBBExtensions.NAMESPACE); + + checkSequence(mess, (int) data.getSeq()); + buffer = StringUtils.decodeBase64(data.getData()); + bufferPointer = 0; + return true; + } + + private void checkSequence(Message mess, int seq) throws IOException { + if (this.seq == 65535) { + this.seq = -1; + } + if (seq - 1 != this.seq) { + cancelTransfer(mess); + throw new IOException("Packets out of sequence"); + } + else { + this.seq = seq; + } + } + + private void cancelTransfer(Message mess) { + cleanup(); + + sendCancelMessage(mess); + } + + private void cleanup() { + dataCollector.cancel(); + connection.removePacketListener(this); + } + + private void sendCancelMessage(Message message) { + IQ error = FileTransferNegotiator.createIQ(message.getPacketID(), message.getFrom(), message.getTo(), + IQ.Type.ERROR); + error.setError(new XMPPError(XMPPError.Condition.remote_server_timeout, "Cancel Message Transfer")); + connection.sendPacket(error); + } + + public boolean markSupported() { + return false; + } + + public void processPacket(Packet packet) { + IBBExtensions.Close close = (IBBExtensions.Close) packet; + if (close.getSessionID().equals(streamID)) { + isDone = true; + closeConfirmation = FileTransferNegotiator.createIQ(packet + .getPacketID(), packet.getFrom(), packet.getTo(), + IQ.Type.RESULT); + } + } + + public synchronized void close() throws IOException { + if (isClosed) { + return; + } + cleanup(); + + if (isEOF) { + sendCloseConfirmation(); + } + else if (lastMess != null) { + sendCancelMessage(lastMess); + } + isClosed = true; + } + + private void sendCloseConfirmation() { + connection.sendPacket(closeConfirmation); + } + } + + private static class IBBOpenSidFilter implements PacketFilter { + + private String sessionID; + + public IBBOpenSidFilter(String sessionID) { + if (sessionID == null) { + throw new IllegalArgumentException("StreamID cannot be null"); + } + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!IBBExtensions.Open.class.isInstance(packet)) { + return false; + } + IBBExtensions.Open open = (IBBExtensions.Open) packet; + String sessionID = open.getSessionID(); + + return (sessionID != null && sessionID.equals(this.sessionID)); + } + } + + private static class IBBMessageSidFilter implements PacketFilter { + + private final String sessionID; + private String from; + + public IBBMessageSidFilter(String from, String sessionID) { + this.from = from; + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!(packet instanceof Message)) { + return false; + } + if (!packet.getFrom().equalsIgnoreCase(from)) { + return false; + } + + IBBExtensions.Data data = (IBBExtensions.Data) packet. + getExtension(IBBExtensions.Data.ELEMENT_NAME, IBBExtensions.NAMESPACE); + return data != null && data.getSessionID() != null + && data.getSessionID().equalsIgnoreCase(sessionID); + } + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/IncomingFileTransfer.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/IncomingFileTransfer.java new file mode 100644 index 000000000..da20e0c24 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/IncomingFileTransfer.java @@ -0,0 +1,219 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.XMPPException; + +import java.io.*; +import java.util.concurrent.*; + +/** + * An incoming file transfer is created when the + * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)} + * method is invoked. It is a file being sent to the local user from another + * user on the jabber network. There are two stages of the file transfer to be + * concerned with and they can be handled in different ways depending upon the + * method that is invoked on this class. + *

+ * The first way that a file is recieved is by calling the + * {@link #recieveFile()} method. This method, negotiates the appropriate stream + * method and then returns the InputStream to read the file + * data from. + *

+ * The second way that a file can be recieved through this class is by invoking + * the {@link #recieveFile(File)} method. This method returns immediatly and + * takes as its parameter a file on the local file system where the file + * recieved from the transfer will be put. + * + * @author Alexander Wenckus + */ +public class IncomingFileTransfer extends FileTransfer { + + private FileTransferRequest recieveRequest; + + private InputStream inputStream; + + protected IncomingFileTransfer(FileTransferRequest request, + FileTransferNegotiator transferNegotiator) { + super(request.getRequestor(), request.getStreamID(), transferNegotiator); + this.recieveRequest = request; + } + + /** + * Negotiates the stream method to transfer the file over and then returns + * the negotiated stream. + * + * @return The negotiated InputStream from which to read the data. + * @throws XMPPException If there is an error in the negotiation process an exception + * is thrown. + */ + public InputStream recieveFile() throws XMPPException { + if (inputStream != null) { + throw new IllegalStateException("Transfer already negotiated!"); + } + + try { + inputStream = negotiateStream(); + } + catch (XMPPException e) { + setException(e); + throw e; + } + + return inputStream; + } + + /** + * This method negotitates the stream and then transfer's the file over the + * negotiated stream. The transfered file will be saved at the provided + * location. + *

+ * This method will return immedialtly, file transfer progress can be + * monitored through several methods: + *

+ *

    + *
  • {@link FileTransfer#getStatus()} + *
  • {@link FileTransfer#getProgress()} + *
  • {@link FileTransfer#isDone()} + *
+ * + * @param file The location to save the file. + * @throws XMPPException when the file transfer fails + * @throws IllegalArgumentException This exception is thrown when the the provided file is + * either null, or cannot be written to. + */ + public void recieveFile(final File file) throws XMPPException { + if (file != null) { + if (!file.exists()) { + try { + file.createNewFile(); + } + catch (IOException e) { + throw new XMPPException( + "Could not create file to write too", e); + } + } + if (!file.canWrite()) { + throw new IllegalArgumentException("Cannot write to provided file"); + } + } + else { + throw new IllegalArgumentException("File cannot be null"); + } + + Thread transferThread = new Thread(new Runnable() { + public void run() { + try { + inputStream = negotiateStream(); + } + catch (XMPPException e) { + handleXMPPException(e); + return; + } + + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + setStatus(Status.in_progress); + writeToStream(inputStream, outputStream); + } + catch (XMPPException e) { + setStatus(Status.error); + setError(Error.stream); + setException(e); + } + catch (FileNotFoundException e) { + setStatus(Status.error); + setError(Error.bad_file); + setException(e); + } + + if (getStatus().equals(Status.in_progress)) { + setStatus(Status.complete); + } + if (inputStream != null) { + try { + inputStream.close(); + } + catch (Throwable io) { + /* Ignore */ + } + } + if (outputStream != null) { + try { + outputStream.close(); + } + catch (Throwable io) { + /* Ignore */ + } + } + } + }, "File Transfer " + streamID); + transferThread.start(); + } + + private void handleXMPPException(XMPPException e) { + setStatus(FileTransfer.Status.error); + setException(e); + } + + private InputStream negotiateStream() throws XMPPException { + setStatus(Status.negotiating_transfer); + final StreamNegotiator streamNegotiator = negotiator + .selectStreamNegotiator(recieveRequest); + setStatus(Status.negotiating_stream); + FutureTask streamNegotiatorTask = new FutureTask( + new Callable() { + + public InputStream call() throws Exception { + System.out.println(new java.util.Date().toString() + ": IncomingFileTransfer>negotiateStream:186"); + return streamNegotiator + .createIncomingStream(recieveRequest.getStreamInitiation()); + } + }); + streamNegotiatorTask.run(); + InputStream inputStream = null; + try { + System.out.println(new java.util.Date().toString() + ": IncomingFileTransfer>negotiateStream:194"); + inputStream = streamNegotiatorTask.get(15, TimeUnit.SECONDS); + System.out.println(new java.util.Date().toString() + ": IncomingFileTransfer>negotiateStream:196"); + } + catch (InterruptedException e) { + // throw new XMPPException("Interruption while executing", e); + } + catch (ExecutionException e) { + System.out.println(new java.util.Date().toString() + ": IncomingFileTransfer>negotiateStream:202 Error in execution"); + // throw new XMPPException("Error in execution", e); + } + catch (TimeoutException e) { + // throw new XMPPException("Request timed out", e); + } + finally { + streamNegotiatorTask.cancel(true); + } + setStatus(Status.negotiated); + return inputStream; + } + + public void cancel() { + setStatus(Status.cancelled); + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/OutgoingFileTransfer.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/OutgoingFileTransfer.java new file mode 100644 index 000000000..f5ef15293 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/OutgoingFileTransfer.java @@ -0,0 +1,387 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.XMPPError; + +import java.io.*; + +/** + * Handles the sending of a file to another user. File transfer's in jabber have + * several steps and there are several methods in this class that handle these + * steps differently. + * + * @author Alexander Wenckus + * + */ +public class OutgoingFileTransfer extends FileTransfer { + + private static int RESPONSE_TIMEOUT = 60 * 1000; + private NegotiationProgress callback; + + /** + * Returns the time in milliseconds after which the file transfer + * negotiation process will timeout if the other user has not responded. + * + * @return Returns the time in milliseconds after which the file transfer + * negotiation process will timeout if the remote user has not + * responded. + */ + public static int getResponseTimeout() { + return RESPONSE_TIMEOUT; + } + + /** + * Sets the time in milliseconds after which the file transfer negotiation + * process will timeout if the other user has not responded. + * + * @param responseTimeout + * The timeout time in milliseconds. + */ + public static void setResponseTimeout(int responseTimeout) { + RESPONSE_TIMEOUT = responseTimeout; + } + + private OutputStream outputStream; + + private String initiator; + + private Thread transferThread; + + protected OutgoingFileTransfer(String initiator, String target, + String streamID, FileTransferNegotiator transferNegotiator) { + super(target, streamID, transferNegotiator); + this.initiator = initiator; + } + + protected void setOutputStream(OutputStream stream) { + if (outputStream == null) { + this.outputStream = stream; + } + } + + /** + * Returns the output stream connected to the peer to transfer the file. It + * is only available after it has been succesfully negotiated by the + * {@link StreamNegotiator}. + * + * @return Returns the output stream connected to the peer to transfer the + * file. + */ + protected OutputStream getOutputStream() { + if (getStatus().equals(FileTransfer.Status.negotiated)) { + return outputStream; + } else { + return null; + } + } + + /** + * This method handles the negotiation of the file transfer and the stream, + * it only returns the created stream after the negotiation has been completed. + * + * @param fileName + * The name of the file that will be transmitted. It is + * preferable for this name to have an extension as it will be + * used to determine the type of file it is. + * @param fileSize + * The size in bytes of the file that will be transmitted. + * @param description + * A description of the file that will be transmitted. + * @return The OutputStream that is connected to the peer to transmit the + * file. + * @throws XMPPException + * Thrown if an error occurs during the file transfer + * negotiation process. + */ + public synchronized OutputStream sendFile(String fileName, long fileSize, + String description) throws XMPPException { + if (isDone() || outputStream != null) { + throw new IllegalStateException( + "The negotation process has already" + + " been attempted on this file transfer"); + } + try { + this.outputStream = negotiateStream(fileName, fileSize, description); + } catch (XMPPException e) { + handleXMPPException(e); + throw e; + } + return outputStream; + } + + /** + * This methods handles the transfer and stream negotiation process. It + * returns immediately and its progress will be updated through the + * {@link NegotiationProgress} callback. + * + * @param fileName + * The name of the file that will be transmitted. It is + * preferable for this name to have an extension as it will be + * used to determine the type of file it is. + * @param fileSize + * The size in bytes of the file that will be transmitted. + * @param description + * A description of the file that will be transmitted. + * @param progress + * A callback to monitor the progress of the file transfer + * negotiation process and to retrieve the OutputStream when it + * is complete. + */ + public synchronized void sendFile(final String fileName, + final long fileSize, final String description, + final NegotiationProgress progress) + { + if(progress == null) { + throw new IllegalArgumentException("Callback progress cannot be null."); + } + checkTransferThread(); + if (isDone() || outputStream != null) { + throw new IllegalStateException( + "The negotation process has already" + + " been attempted for this file transfer"); + } + this.callback = progress; + transferThread = new Thread(new Runnable() { + public void run() { + try { + OutgoingFileTransfer.this.outputStream = negotiateStream( + fileName, fileSize, description); + progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream); + } + catch (XMPPException e) { + handleXMPPException(e); + } + } + }, "File Transfer Negotiation " + streamID); + transferThread.start(); + } + + private void checkTransferThread() { + if (transferThread != null && transferThread.isAlive() || isDone()) { + throw new IllegalStateException( + "File transfer in progress or has already completed."); + } + } + + /** + * This method handles the stream negotiation process and transmits the file + * to the remote user. It returns immediatly and the progress of the file + * transfer can be monitored through several methods: + * + *
    + *
  • {@link FileTransfer#getStatus()} + *
  • {@link FileTransfer#getProgress()} + *
  • {@link FileTransfer#isDone()} + *
+ * + * @param file the file to transfer to the remote entity. + * @param description a description for the file to transfer. + * @throws XMPPException + * If there is an error during the negotiation process or the + * sending of the file. + */ + public synchronized void sendFile(final File file, final String description) + throws XMPPException { + checkTransferThread(); + if (file == null || !file.exists() || !file.canRead()) { + throw new IllegalArgumentException("Could not read file"); + } else { + setFileInfo(file.getAbsolutePath(), file.getName(), file.length()); + } + + transferThread = new Thread(new Runnable() { + public void run() { + try { + outputStream = negotiateStream(file.getName(), file + .length(), description); + } catch (XMPPException e) { + handleXMPPException(e); + return; + } + if (outputStream == null) { + return; + } + + if (!updateStatus(Status.negotiated, Status.in_progress)) { + return; + } + + InputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + writeToStream(inputStream, outputStream); + } catch (FileNotFoundException e) { + setStatus(FileTransfer.Status.error); + setError(Error.bad_file); + setException(e); + } catch (XMPPException e) { + setStatus(FileTransfer.Status.error); + setException(e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + + outputStream.flush(); + outputStream.close(); + } catch (IOException e) { + /* Do Nothing */ + } + } + updateStatus(Status.in_progress, FileTransfer.Status.complete); + } + + }, "File Transfer " + streamID); + transferThread.start(); + } + + private void handleXMPPException(XMPPException e) { + XMPPError error = e.getXMPPError(); + if (error != null) { + int code = error.getCode(); + if (code == 403) { + setStatus(Status.refused); + return; + } + else if (code == 400) { + setStatus(Status.error); + setError(Error.not_acceptable); + } + else { + setStatus(FileTransfer.Status.error); + } + } + + setException(e); + } + + /** + * Returns the amount of bytes that have been sent for the file transfer. Or + * -1 if the file transfer has not started. + *

+ * Note: This method is only useful when the {@link #sendFile(File, String)} + * method is called, as it is the only method that actualy transmits the + * file. + * + * @return Returns the amount of bytes that have been sent for the file + * transfer. Or -1 if the file transfer has not started. + */ + public long getBytesSent() { + return amountWritten; + } + + private OutputStream negotiateStream(String fileName, long fileSize, + String description) throws XMPPException { + // Negotiate the file transfer profile + + if (!updateStatus(Status.initial, Status.negotiating_transfer)) { + throw new XMPPException("Illegal state change"); + } + StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer( + getPeer(), streamID, fileName, fileSize, description, + RESPONSE_TIMEOUT); + + if (streamNegotiator == null) { + setStatus(Status.error); + setError(Error.no_response); + return null; + } + + // Negotiate the stream + if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) { + throw new XMPPException("Illegal state change"); + } + outputStream = streamNegotiator.createOutgoingStream(streamID, + initiator, getPeer()); + + if (!updateStatus(Status.negotiating_stream, Status.negotiated)) { + throw new XMPPException("Illegal state change"); + } + return outputStream; + } + + public void cancel() { + setStatus(Status.cancelled); + } + + @Override + protected boolean updateStatus(Status oldStatus, Status newStatus) { + boolean isUpdated = super.updateStatus(oldStatus, newStatus); + if(callback != null && isUpdated) { + callback.statusUpdated(oldStatus, newStatus); + } + return isUpdated; + } + + @Override + protected void setStatus(Status status) { + Status oldStatus = getStatus(); + super.setStatus(status); + if(callback != null) { + callback.statusUpdated(oldStatus, status); + } + } + + @Override + protected void setException(Exception exception) { + super.setException(exception); + if(callback != null) { + callback.errorEstablishingStream(exception); + } + } + + /** + * A callback class to retrive the status of an outgoing transfer + * negotiation process. + * + * @author Alexander Wenckus + * + */ + public interface NegotiationProgress { + + /** + * Called when the status changes + * + * @param oldStatus the previous status of the file transfer. + * @param newStatus the new status of the file transfer. + */ + void statusUpdated(Status oldStatus, Status newStatus); + + /** + * Once the negotiation process is completed the output stream can be + * retrieved. + * + * @param stream the established stream which can be used to transfer the file to the remote + * entity + */ + void outputStreamEstablished(OutputStream stream); + + /** + * Called when an exception occurs during the negotiation progress. + * + * @param e the exception that occured. + */ + void errorEstablishingStream(Exception e); + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/Socks5TransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/Socks5TransferNegotiator.java new file mode 100644 index 000000000..a98771292 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/Socks5TransferNegotiator.java @@ -0,0 +1,576 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromMatchesFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.Bytestream; +import org.jivesoftware.smackx.packet.Bytestream.StreamHost; +import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.*; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Iterator; +import java.util.Date; + +/** + * A SOCKS5 bytestream is negotiated partly over the XMPP XML stream and partly + * over a seperate socket. The actual transfer though takes place over a + * seperatly created socket. + *

+ * A SOCKS5 file transfer generally has three parites, the initiator, the + * target, and the stream host. The stream host is a specialized SOCKS5 proxy + * setup on the server, or, the Initiator can act as the Stream Host if the + * proxy is not available. + *

+ * The advantage of having a seperate proxy over directly connecting to + * eachother is if the Initator and the Target are not on the same LAN and are + * operating behind NAT, the proxy allows for a common location for both parties + * to connect to and transfer the file. + *

+ * Smack will attempt to automatically discover any proxies present on your + * server. If any are detected they will be forwarded to any user attempting to + * recieve files from you. + * + * @author Alexander Wenckus + * @see JEP-0065: SOCKS5 + * Bytestreams + */ +public class Socks5TransferNegotiator extends StreamNegotiator { + + protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams"; + + /** + * The number of connection failures it takes to a streamhost for that particular streamhost + * to be blacklisted. When a host is blacklisted no more connection attempts will be made to + * it for a period of 2 hours. + */ + private static final int CONNECT_FAILURE_THRESHOLD = 2; + + public static boolean isAllowLocalProxyHost = true; + + private final XMPPConnection connection; + + private Socks5TransferNegotiatorManager transferNegotiatorManager; + + public Socks5TransferNegotiator(Socks5TransferNegotiatorManager transferNegotiatorManager, + final XMPPConnection connection) + { + this.connection = connection; + this.transferNegotiatorManager = transferNegotiatorManager; + } + + public PacketFilter getInitiationPacketFilter(String from, String sessionID) { + return new AndFilter(new FromMatchesFilter(from), + new BytestreamSIDFilter(sessionID)); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload( + * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File) + */ + InputStream negotiateIncomingStream(Packet streamInitiation) + throws XMPPException { + Bytestream streamHostsInfo = (Bytestream) streamInitiation; + + if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(streamHostsInfo.getError()); + } + SelectedHostInfo selectedHost; + try { + // select appropriate host + System.out.println(new java.util.Date().toString() + ": Socks5TransferNegotiator>negotiateIncomingStream:115"); + selectedHost = selectHost(streamHostsInfo); + System.out.println(new java.util.Date().toString() + ": Socks5TransferNegotiator>negotiateIncomingStream:117"); + } + catch (XMPPException ex) { + if (ex.getXMPPError() != null) { + IQ errorPacket = super.createError(streamHostsInfo.getTo(), + streamHostsInfo.getFrom(), streamHostsInfo.getPacketID(), + ex.getXMPPError()); + connection.sendPacket(errorPacket); + } + throw (ex); + } + + // send used-host confirmation + Bytestream streamResponse = createUsedHostConfirmation( + selectedHost.selectedHost, streamHostsInfo.getFrom(), + streamHostsInfo.getTo(), streamHostsInfo.getPacketID()); + connection.sendPacket(streamResponse); + + try { + PushbackInputStream stream = new PushbackInputStream( + selectedHost.establishedSocket.getInputStream()); + int firstByte = stream.read(); + stream.unread(firstByte); + return stream; + } + catch (IOException e) { + throw new XMPPException("Error establishing input stream", e); + } + + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + Packet streamInitiation = initiateIncomingStream(connection, initiation); + return negotiateIncomingStream(streamInitiation); + } + + /** + * The used host confirmation is sent to the initiator to indicate to them + * which of the hosts they provided has been selected and successfully + * connected to. + * + * @param selectedHost The selected stream host. + * @param initiator The initiator of the stream. + * @param target The target of the stream. + * @param packetID The of the packet being responded to. + * @return The packet that was created to send to the initiator. + */ + private Bytestream createUsedHostConfirmation(StreamHost selectedHost, + String initiator, String target, String packetID) { + Bytestream streamResponse = new Bytestream(); + streamResponse.setTo(initiator); + streamResponse.setFrom(target); + streamResponse.setType(IQ.Type.RESULT); + streamResponse.setPacketID(packetID); + streamResponse.setUsedHost(selectedHost.getJID()); + return streamResponse; + } + + /** + * Selects a host to connect to over which the file will be transmitted. + * + * @param streamHostsInfo the packet recieved from the initiator containing the available hosts + * to transfer the file + * @return the selected host and socket that were created. + * @throws XMPPException when there is no appropriate host. + */ + private SelectedHostInfo selectHost(Bytestream streamHostsInfo) + throws XMPPException { + Iterator it = streamHostsInfo.getStreamHosts().iterator(); + StreamHost selectedHost = null; + Socket socket = null; + while (it.hasNext()) { + selectedHost = (StreamHost) it.next(); + String address = selectedHost.getAddress(); + + // Check to see if this address has been blacklisted + int failures = getConnectionFailures(address); + if (failures >= CONNECT_FAILURE_THRESHOLD) { + continue; + } + // establish socket + try { + socket = new Socket(address, selectedHost + .getPort()); + establishSOCKS5ConnectionToProxy(socket, createDigest( + streamHostsInfo.getSessionID(), streamHostsInfo + .getFrom(), streamHostsInfo.getTo())); + break; + } + catch (IOException e) { + e.printStackTrace(); + incrementConnectionFailures(address); + selectedHost = null; + socket = null; + } + } + if (selectedHost == null || socket == null || !socket.isConnected()) { + String errorMessage = "Could not establish socket with any provided host"; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.no_acceptable, errorMessage)); + } + + return new SelectedHostInfo(selectedHost, socket); + } + + private void incrementConnectionFailures(String address) { + transferNegotiatorManager.incrementConnectionFailures(address); + } + + private int getConnectionFailures(String address) { + return transferNegotiatorManager.getConnectionFailures(address); + } + + /** + * Creates the digest needed for a byte stream. It is the SHA1(sessionID + + * initiator + target). + * + * @param sessionID The sessionID of the stream negotiation + * @param initiator The inititator of the stream negotiation + * @param target The target of the stream negotiation + * @return SHA-1 hash of the three parameters + */ + private String createDigest(final String sessionID, final String initiator, + final String target) { + return StringUtils.hash(sessionID + StringUtils.parseName(initiator) + + "@" + StringUtils.parseServer(initiator) + "/" + + StringUtils.parseResource(initiator) + + StringUtils.parseName(target) + "@" + + StringUtils.parseServer(target) + "/" + + StringUtils.parseResource(target)); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateUpload(java.lang.String, + * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File) + */ + public OutputStream createOutgoingStream(String streamID, String initiator, + String target) throws XMPPException + { + Socket socket; + try { + socket = initBytestreamSocket(streamID, initiator, target); + } + catch (Exception e) { + throw new XMPPException("Error establishing transfer socket", e); + } + + if (socket != null) { + try { + return new BufferedOutputStream(socket.getOutputStream()); + } + catch (IOException e) { + throw new XMPPException("Error establishing output stream", e); + } + } + return null; + } + + private Socket initBytestreamSocket(final String sessionID, + String initiator, String target) throws Exception { + Socks5TransferNegotiatorManager.ProxyProcess process; + try { + process = establishListeningSocket(); + } + catch (IOException io) { + process = null; + } + + Socket conn; + try { + String localIP; + try { + localIP = discoverLocalIP(); + } + catch (UnknownHostException e1) { + localIP = null; + } + + Bytestream query = createByteStreamInit(initiator, target, sessionID, + localIP, (process != null ? process.getPort() : 0)); + + // if the local host is one of the options we need to wait for the + // remote connection. + conn = waitForUsedHostResponse(sessionID, process, createDigest( + sessionID, initiator, target), query).establishedSocket; + } + finally { + cleanupListeningSocket(); + } + + return conn; + } + + + /** + * Waits for the peer to respond with which host they chose to use. + * + * @param sessionID The session id of the stream. + * @param proxy The server socket which will listen locally for remote + * connections. + * @param digest the digest of the userids and the session id + * @param query the query which the response is being awaited + * @return the selected host + * @throws XMPPException when the response from the peer is an error or doesn't occur + * @throws IOException when there is an error establishing the local socket + */ + private SelectedHostInfo waitForUsedHostResponse(String sessionID, + final Socks5TransferNegotiatorManager.ProxyProcess proxy, final String digest, + final Bytestream query) throws XMPPException, IOException + { + SelectedHostInfo info = new SelectedHostInfo(); + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(query.getPacketID())); + connection.sendPacket(query); + + Packet packet = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + Bytestream response; + if (packet != null && packet instanceof Bytestream) { + response = (Bytestream) packet; + } + else { + throw new XMPPException("Unexpected response from remote user"); + } + + // check for an error + if (response.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException("Remote client returned error, stream hosts expected", + response.getError()); + } + + StreamHostUsed used = response.getUsedHost(); + StreamHost usedHost = query.getStreamHost(used.getJID()); + if (usedHost == null) { + throw new XMPPException("Remote user responded with unknown host"); + } + // The local computer is acting as the proxy + if (used.getJID().equals(query.getFrom())) { + info.establishedSocket = proxy.getSocket(digest); + info.selectedHost = usedHost; + return info; + } + else { + info.establishedSocket = new Socket(usedHost.getAddress(), usedHost + .getPort()); + establishSOCKS5ConnectionToProxy(info.establishedSocket, digest); + + Bytestream activate = createByteStreamActivate(sessionID, response + .getTo(), usedHost.getJID(), response.getFrom()); + + collector = connection.createPacketCollector(new PacketIDFilter( + activate.getPacketID())); + connection.sendPacket(activate); + + IQ serverResponse = (IQ) collector.nextResult(SmackConfiguration + .getPacketReplyTimeout()); + collector.cancel(); + if (!serverResponse.getType().equals(IQ.Type.RESULT)) { + info.establishedSocket.close(); + return null; + } + return info; + } + } + + private Socks5TransferNegotiatorManager.ProxyProcess establishListeningSocket() + throws IOException { + return transferNegotiatorManager.addTransfer(); + } + + private void cleanupListeningSocket() { + transferNegotiatorManager.removeTransfer(); + } + + private String discoverLocalIP() throws UnknownHostException { + return InetAddress.getLocalHost().getHostAddress(); + } + + /** + * The bytestream init looks like this: + *

+ *

+     * <iq type='set'
+     *     from='initiator@host1/foo'
+     *     to='target@host2/bar'
+     *     id='initiate'>
+     *   <query xmlns='http://jabber.org/protocol/bytestreams'
+     *          sid='mySID'
+     * 	 mode='tcp'>
+     *     <streamhost
+     *         jid='initiator@host1/foo'
+     *         host='192.168.4.1'
+     *        port='5086'/>
+     *     <streamhost
+     *         jid='proxy.host3'
+     *         host='24.24.24.1'
+     *         zeroconf='_jabber.bytestreams'/>
+     *   </query>
+     * </iq>
+     * 
+ * + * @param from initiator@host1/foo - the file transfer initiator. + * @param to target@host2/bar - the file transfer target. + * @param sid 'mySID' - the unique identifier for this file transfer + * @param localIP the IP of the local machine if it is being provided, null otherwise. + * @param port the port of the local mahine if it is being provided, null otherwise. + * @return the created Bytestream packet + */ + private Bytestream createByteStreamInit(final String from, final String to, + final String sid, final String localIP, final int port) + { + Bytestream bs = new Bytestream(); + bs.setTo(to); + bs.setFrom(from); + bs.setSessionID(sid); + bs.setType(IQ.Type.SET); + bs.setMode(Bytestream.Mode.tcp); + if (localIP != null && port > 0) { + bs.addStreamHost(from, localIP, port); + } + // make sure the proxies have been initialized completely + Collection streamHosts = transferNegotiatorManager.getStreamHosts(); + + if (streamHosts != null) { + for (StreamHost host : streamHosts) { + bs.addStreamHost(host); + } + } + + return bs; + } + + + /** + * Returns the packet to send notification to the stream host to activate + * the stream. + * + * @param sessionID the session ID of the file transfer to activate. + * @param from the sender of the bytestreeam + * @param to the JID of the stream host + * @param target the JID of the file transfer target. + * @return the packet to send notification to the stream host to + * activate the stream. + */ + private static Bytestream createByteStreamActivate(final String sessionID, + final String from, final String to, final String target) + { + Bytestream activate = new Bytestream(sessionID); + activate.setMode(null); + activate.setToActivate(target); + activate.setFrom(from); + activate.setTo(to); + activate.setType(IQ.Type.SET); + return activate; + } + + public String[] getNamespaces() { + return new String[]{NAMESPACE}; + } + + private void establishSOCKS5ConnectionToProxy(Socket socket, String digest) + throws IOException { + + byte[] cmd = new byte[3]; + + cmd[0] = (byte) 0x05; + cmd[1] = (byte) 0x01; + cmd[2] = (byte) 0x00; + + OutputStream out = new DataOutputStream(socket.getOutputStream()); + out.write(cmd); + + InputStream in = new DataInputStream(socket.getInputStream()); + byte[] response = new byte[2]; + + in.read(response); + + cmd = createOutgoingSocks5Message(1, digest); + out.write(cmd); + createIncomingSocks5Message(in); + } + + static String createIncomingSocks5Message(InputStream in) + throws IOException { + byte[] cmd = new byte[5]; + in.read(cmd, 0, 5); + + byte[] addr = new byte[cmd[4]]; + in.read(addr, 0, addr.length); + String digest = new String(addr); + in.read(); + in.read(); + + return digest; + } + + static byte[] createOutgoingSocks5Message(int cmd, String digest) { + byte addr[] = digest.getBytes(); + + byte[] data = new byte[7 + addr.length]; + data[0] = (byte) 5; + data[1] = (byte) cmd; + data[2] = (byte) 0; + data[3] = (byte) 0x3; + data[4] = (byte) addr.length; + + System.arraycopy(addr, 0, data, 5, addr.length); + data[data.length - 2] = (byte) 0; + data[data.length - 1] = (byte) 0; + + return data; + } + + public void cleanup() { + + } + + private static class SelectedHostInfo { + + protected XMPPException exception; + + protected StreamHost selectedHost; + + protected Socket establishedSocket; + + SelectedHostInfo(StreamHost selectedHost, Socket establishedSocket) { + this.selectedHost = selectedHost; + this.establishedSocket = establishedSocket; + } + + public SelectedHostInfo() { + } + } + + + private static class BytestreamSIDFilter implements PacketFilter { + + private String sessionID; + + public BytestreamSIDFilter(String sessionID) { + if (sessionID == null) { + throw new IllegalArgumentException("StreamID cannot be null"); + } + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!Bytestream.class.isInstance(packet)) { + return false; + } + Bytestream bytestream = (Bytestream) packet; + String sessionID = bytestream.getSessionID(); + + return (sessionID != null && sessionID.equals(this.sessionID)); + } + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/Socks5TransferNegotiatorManager.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/Socks5TransferNegotiatorManager.java new file mode 100644 index 000000000..35f2c3509 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/Socks5TransferNegotiatorManager.java @@ -0,0 +1,388 @@ +/** + * $Revision:$ + * $Date:$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.util.Cache; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.Bytestream; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.*; +import java.io.*; + +/** + * + */ +public class Socks5TransferNegotiatorManager implements FileTransferNegotiatorManager { + + private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120; + // locks the proxies during their initialization process + private final Object proxyLock = new Object(); + + private static ProxyProcess proxyProcess; + + // locks on the proxy process during its initiatilization process + private final Object processLock = new Object(); + + private final Cache addressBlacklist + = new Cache(100, BLACKLIST_LIFETIME); + + private XMPPConnection connection; + + private List proxies; + + private List streamHosts; + + public Socks5TransferNegotiatorManager(XMPPConnection connection) { + this.connection = connection; + } + + public StreamNegotiator createNegotiator() { + return new Socks5TransferNegotiator(this, connection); + } + + public void incrementConnectionFailures(String address) { + Integer count = addressBlacklist.get(address); + if (count == null) { + count = 1; + } + else { + count += 1; + } + addressBlacklist.put(address, count); + } + + public int getConnectionFailures(String address) { + Integer count = addressBlacklist.get(address); + return count != null ? count : 0; + } + + public ProxyProcess addTransfer() throws IOException { + synchronized (processLock) { + if (proxyProcess == null) { + proxyProcess = new ProxyProcess(new ServerSocket(7777)); + proxyProcess.start(); + } + } + proxyProcess.addTransfer(); + return proxyProcess; + } + + public void removeTransfer() { + if (proxyProcess == null) { + return; + } + proxyProcess.removeTransfer(); + } + + public Collection getStreamHosts() { + synchronized (proxyLock) { + if (proxies == null) { + initProxies(); + } + } + return Collections.unmodifiableCollection(streamHosts); + } + + /** + * Checks the service discovery item returned from a server component to verify if it is + * a File Transfer proxy or not. + * + * @param manager the service discovery manager which will be used to query the component + * @param item the discovered item on the server relating + * @return returns the JID of the proxy if it is a proxy or null if the item is not a proxy. + */ + private String checkIsProxy(ServiceDiscoveryManager manager, DiscoverItems.Item item) { + DiscoverInfo info; + try { + info = manager.discoverInfo(item.getEntityID()); + } + catch (XMPPException e) { + return null; + } + Iterator itx = info.getIdentities(); + while (itx.hasNext()) { + DiscoverInfo.Identity identity = (DiscoverInfo.Identity) itx.next(); + if ("proxy".equalsIgnoreCase(identity.getCategory()) + && "bytestreams".equalsIgnoreCase( + identity.getType())) { + return info.getFrom(); + } + } + return null; + } + + private void initProxies() { + proxies = new ArrayList(); + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + try { + DiscoverItems discoItems = manager.discoverItems(connection.getServiceName()); + Iterator it = discoItems.getItems(); + while (it.hasNext()) { + DiscoverItems.Item item = (DiscoverItems.Item) it.next(); + String proxy = checkIsProxy(manager, item); + if (proxy != null) { + proxies.add(proxy); + } + } + } + catch (XMPPException e) { + return; + } + if (proxies.size() > 0) { + initStreamHosts(); + } + } + + /** + * Loads streamhost address and ports from the proxies on the local server. + */ + private void initStreamHosts() { + List streamHosts = new ArrayList(); + Iterator it = proxies.iterator(); + IQ query; + PacketCollector collector; + Bytestream response; + while (it.hasNext()) { + String jid = it.next().toString(); + query = new IQ() { + public String getChildElementXML() { + return ""; + } + }; + query.setType(IQ.Type.GET); + query.setTo(jid); + + collector = connection.createPacketCollector(new PacketIDFilter( + query.getPacketID())); + connection.sendPacket(query); + + response = (Bytestream) collector.nextResult(SmackConfiguration + .getPacketReplyTimeout()); + if (response != null) { + streamHosts.addAll(response.getStreamHosts()); + } + collector.cancel(); + } + this.streamHosts = streamHosts; + } + + public void cleanup() { + synchronized (processLock) { + if (proxyProcess != null) { + proxyProcess.stop(); + proxyProcess = null; + } + } + } + + class ProxyProcess implements Runnable { + + private final ServerSocket listeningSocket; + + private final Map connectionMap = new HashMap(); + + private boolean done = false; + + private Thread thread; + private int transfers; + + public void run() { + try { + try { + listeningSocket.setSoTimeout(10000); + } + catch (SocketException e) { + // There was a TCP error, lets print the stack trace + e.printStackTrace(); + return; + } + while (!done) { + Socket conn = null; + synchronized (ProxyProcess.this) { + while (transfers <= 0 && !done) { + transfers = -1; + try { + ProxyProcess.this.wait(); + } + catch (InterruptedException e) { + /* Do nothing */ + } + } + } + if (done) { + break; + } + try { + synchronized (listeningSocket) { + conn = listeningSocket.accept(); + } + if (conn == null) { + continue; + } + String digest = establishSocks5UploadConnection(conn); + synchronized (connectionMap) { + connectionMap.put(digest, conn); + } + } + catch (SocketTimeoutException e) { + /* Do Nothing */ + } + catch (IOException e) { + /* Do Nothing */ + } + catch (XMPPException e) { + e.printStackTrace(); + if (conn != null) { + try { + conn.close(); + } + catch (IOException e1) { + /* Do Nothing */ + } + } + } + } + } + finally { + try { + listeningSocket.close(); + } + catch (IOException e) { + /* Do Nothing */ + } + } + } + + /** + * Negotiates the Socks 5 bytestream when the local computer is acting as + * the proxy. + * + * @param connection the socket connection with the peer. + * @return the SHA-1 digest that is used to uniquely identify the file + * transfer. + * @throws XMPPException + * @throws IOException + */ + private String establishSocks5UploadConnection(Socket connection) throws XMPPException, IOException { + OutputStream out = new DataOutputStream(connection.getOutputStream()); + InputStream in = new DataInputStream(connection.getInputStream()); + + // first byte is version should be 5 + int b = in.read(); + if (b != 5) { + throw new XMPPException("Only SOCKS5 supported"); + } + + // second byte number of authentication methods supported + b = in.read(); + int[] auth = new int[b]; + for (int i = 0; i < b; i++) { + auth[i] = in.read(); + } + + int authMethod = -1; + for (int anAuth : auth) { + authMethod = (anAuth == 0 ? 0 : -1); // only auth method + // 0, no + // authentication, + // supported + if (authMethod == 0) { + break; + } + } + if (authMethod != 0) { + throw new XMPPException("Authentication method not supported"); + } + byte[] cmd = new byte[2]; + cmd[0] = (byte) 0x05; + cmd[1] = (byte) 0x00; + out.write(cmd); + + String responseDigest = Socks5TransferNegotiator.createIncomingSocks5Message(in); + cmd = Socks5TransferNegotiator.createOutgoingSocks5Message(0, responseDigest); + + if (!connection.isConnected()) { + throw new XMPPException("Socket closed by remote user"); + } + out.write(cmd); + return responseDigest; + } + + + public void start() { + thread.start(); + } + + public void stop() { + done = true; + synchronized (this) { + this.notify(); + } + synchronized (listeningSocket) { + listeningSocket.notify(); + } + } + + public int getPort() { + return listeningSocket.getLocalPort(); + } + + ProxyProcess(ServerSocket listeningSocket) { + thread = new Thread(this, "File Transfer Connection Listener"); + this.listeningSocket = listeningSocket; + } + + public Socket getSocket(String digest) { + synchronized (connectionMap) { + return connectionMap.get(digest); + } + } + + public void addTransfer() { + synchronized (this) { + if (transfers == -1) { + transfers = 1; + this.notify(); + } + else { + transfers++; + } + } + } + + public void removeTransfer() { + synchronized (this) { + transfers--; + } + } + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/StreamNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/StreamNegotiator.java new file mode 100644 index 000000000..f5021a451 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/CopyOffiletransfer/StreamNegotiator.java @@ -0,0 +1,164 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date: $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * After the file transfer negotiation process is completed according to + * JEP-0096, the negotation process is passed off to a particular stream + * negotiator. The stream negotiator will then negotiate the chosen stream and + * return the stream to transfer the file. + * + * @author Alexander Wenckus + */ +public abstract class StreamNegotiator { + + /** + * Creates the initiation acceptance packet to forward to the stream + * initiator. + * + * @param streamInitiationOffer The offer from the stream initatior to connect for a stream. + * @param namespaces The namespace that relates to the accepted means of transfer. + * @return The response to be forwarded to the initator. + */ + public StreamInitiation createInitiationAccept( + StreamInitiation streamInitiationOffer, String[] namespaces) + { + StreamInitiation response = new StreamInitiation(); + response.setTo(streamInitiationOffer.getFrom()); + response.setFrom(streamInitiationOffer.getTo()); + response.setType(IQ.Type.RESULT); + response.setPacketID(streamInitiationOffer.getPacketID()); + + DataForm form = new DataForm(Form.TYPE_SUBMIT); + FormField field = new FormField( + FileTransferNegotiator.STREAM_DATA_FIELD_NAME); + for (String namespace : namespaces) { + field.addValue(namespace); + } + form.addField(field); + + response.setFeatureNegotiationForm(form); + return response; + } + + + public IQ createError(String from, String to, String packetID, XMPPError xmppError) { + IQ iq = FileTransferNegotiator.createIQ(packetID, to, from, IQ.Type.ERROR); + iq.setError(xmppError); + return iq; + } + + Packet initiateIncomingStream(XMPPConnection connection, StreamInitiation initiation) throws XMPPException { + StreamInitiation response = createInitiationAccept(initiation, + getNamespaces()); + + // establish collector to await response + PacketCollector collector = connection + .createPacketCollector(getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID())); + connection.sendPacket(response); + + Packet streamMethodInitiation = collector + .nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (streamMethodInitiation == null) { + throw new XMPPException("No response from file transfer initiator"); + } + + return streamMethodInitiation; + } + + /** + * Returns the packet filter that will return the initiation packet for the appropriate stream + * initiation. + * + * @param from The initiatior of the file transfer. + * @param streamID The stream ID related to the transfer. + * @return The PacketFilter that will return the packet relatable to the stream + * initiation. + */ + public abstract PacketFilter getInitiationPacketFilter(String from, String streamID); + + + abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException; + + /** + * This method handles the file stream download negotiation process. The + * appropriate stream negotiator's initiate incoming stream is called after + * an appropriate file transfer method is selected. The manager will respond + * to the initatior with the selected means of transfer, then it will handle + * any negotation specific to the particular transfer method. This method + * returns the InputStream, ready to transfer the file. + * + * @param initiation The initation that triggered this download. + * @return After the negotation process is complete, the InputStream to + * write a file to is returned. + * @throws XMPPException If an error occurs during this process an XMPPException is + * thrown. + */ + public abstract InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException; + + /** + * This method handles the file upload stream negotiation process. The + * particular stream negotiator is determined during the file transfer + * negotiation process. This method returns the OutputStream to transmit the + * file to the remote user. + * + * @param streamID The streamID that uniquely identifies the file transfer. + * @param initiator The fully-qualified JID of the initiator of the file transfer. + * @param target The fully-qualified JID of the target or reciever of the file + * transfer. + * @return The negotiated stream ready for data. + * @throws XMPPException If an error occurs during the negotiation process an + * exception will be thrown. + */ + public abstract OutputStream createOutgoingStream(String streamID, + String initiator, String target) throws XMPPException; + + /** + * Returns the XMPP namespace reserved for this particular type of file + * transfer. + * + * @return Returns the XMPP namespace reserved for this particular type of + * file transfer. + */ + public abstract String[] getNamespaces(); + + /** + * Cleanup any and all resources associated with this negotiator. + */ + public abstract void cleanup(); + +}