diff options
Diffstat (limited to 'src/com/isode/stroke/filetransfer/OutgoingJingleFileTransfer.java')
-rw-r--r-- | src/com/isode/stroke/filetransfer/OutgoingJingleFileTransfer.java | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/src/com/isode/stroke/filetransfer/OutgoingJingleFileTransfer.java b/src/com/isode/stroke/filetransfer/OutgoingJingleFileTransfer.java new file mode 100644 index 0000000..10760a1 --- /dev/null +++ b/src/com/isode/stroke/filetransfer/OutgoingJingleFileTransfer.java @@ -0,0 +1,446 @@ +/* + * Copyright (c) 2011 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ +/* + * Copyright (c) 2013-2015 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ +/* + * Copyright (c) 2015 Tarun Gupta. + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +package com.isode.stroke.filetransfer; + +import com.isode.stroke.jid.JID; +import com.isode.stroke.jingle.JingleSession; +import com.isode.stroke.network.TimerFactory; +import com.isode.stroke.network.Timer; +import com.isode.stroke.crypto.CryptoProvider; +import com.isode.stroke.base.IDGenerator; +import com.isode.stroke.base. ByteArray; +import com.isode.stroke.elements.JingleFileTransferFileInfo; +import com.isode.stroke.elements.JingleDescription; +import com.isode.stroke.elements.JingleTransportPayload; +import com.isode.stroke.elements.JingleIBBTransportPayload; +import com.isode.stroke.elements.JingleS5BTransportPayload; +import com.isode.stroke.elements.JinglePayload; +import com.isode.stroke.elements.JingleContentPayload; +import com.isode.stroke.elements.JingleFileTransferDescription; +import com.isode.stroke.elements.HashElement; +import com.isode.stroke.elements.JingleFileTransferHash; +import com.isode.stroke.jingle.JingleContentID; +import com.isode.stroke.signals.SignalConnection; +import com.isode.stroke.signals.Slot1; +import com.isode.stroke.signals.Slot; +import java.util.logging.Logger; +import java.util.Vector; + +public class OutgoingJingleFileTransfer extends JingleFileTransfer implements OutgoingFileTransfer { + + private long fileSizeInBytes = 0; //FileTransferVariables + private String filename = ""; //FileTransferVariables + + /** + * FileTransferMethod. + */ + @Override + public String getFileName() { + return filename; + } + + /** + * FileTransferMethod. + */ + @Override + public long getFileSizeInBytes() { + return fileSizeInBytes; + } + + /** + * FileTransferMethod. + */ + @Override + public void setFileInfo(final String name, long size) { + this.filename = name; + this.fileSizeInBytes = size; + } + + public static final int DEFAULT_BLOCK_SIZE = 4096; + private IDGenerator idGenerator; + private ReadBytestream stream; + private JingleFileTransferFileInfo fileInfo; + private FileTransferOptions options; + private JingleContentID contentID; + private IncrementalBytestreamHashCalculator hashCalculator; + private State state; + private boolean candidateAcknowledged; + + private Timer waitForRemoteTermination; + + private SignalConnection processedBytesConnection; + private SignalConnection transferFinishedConnection; + private Logger logger_ = Logger.getLogger(this.getClass().getName()); + + public OutgoingJingleFileTransfer( + final JID toJID, + JingleSession session, + ReadBytestream stream, + FileTransferTransporterFactory transporterFactory, + TimerFactory timerFactory, + IDGenerator idGenerator, + final JingleFileTransferFileInfo fileInfo, + final FileTransferOptions options, + CryptoProvider crypto) { + super(session, toJID, transporterFactory); + this.idGenerator = idGenerator; + this.stream = stream; + this.fileInfo = fileInfo; + this.session = session; + this.contentID = new JingleContentID(idGenerator.generateID(), JingleContentPayload.Creator.InitiatorCreator); + this.state = State.Initial; + this.candidateAcknowledged = false; + setFileInfo(fileInfo.getName(), fileInfo.getSize()); + + // calculate both, MD5 and SHA-1 since we don't know which one the other side supports + hashCalculator = new IncrementalBytestreamHashCalculator(true, true, crypto); + stream.onRead.connect(new Slot1<ByteArray>() { + @Override + public void call(ByteArray b) { + hashCalculator.feedData(b); + } + }); + waitForRemoteTermination = timerFactory.createTimer(5000); + waitForRemoteTermination.onTick.connect(new Slot() { + @Override + public void call() { + handleWaitForRemoteTerminationTimeout(); + } + }); + } + + /** + * OutgoingFileTransferMethod. + */ + @Override + public void start() { + logger_.fine("\n"); + if (!State.Initial.equals(state)) { + logger_.warning("Incorrect state\n"); + return; + } + + setTransporter(transporterFactory.createInitiatorTransporter(getInitiator(), getResponder(), options)); + setState(State.GeneratingInitialLocalCandidates); + transporter.startGeneratingLocalCandidates(); + } + + /** + * JingleFileTransferMethod. + */ + @Override + public void cancel() { + terminate(JinglePayload.Reason.Type.Cancel); + } + + private enum State { + Initial, + GeneratingInitialLocalCandidates, + WaitingForAccept, + TryingCandidates, + WaitingForPeerProxyActivate, + WaitingForLocalProxyActivate, + WaitingForCandidateAcknowledge, + FallbackRequested, + Transferring, + WaitForTermination, + Finished + }; + + public void handleSessionAcceptReceived(final JingleContentID contentID, JingleDescription description, JingleTransportPayload transportPayload) { + logger_.fine("\n"); + if (!State.WaitingForAccept.equals(state)) { logger_.warning("Incorrect state\n"); return; } + + if (transportPayload instanceof JingleS5BTransportPayload) { + JingleS5BTransportPayload s5bPayload = (JingleS5BTransportPayload)transportPayload; + transporter.addRemoteCandidates(s5bPayload.getCandidates(), s5bPayload.getDstAddr()); + setState(State.TryingCandidates); + transporter.startTryingRemoteCandidates(); + } + else { + logger_.fine("Unknown transport payload. Falling back.\n"); + fallback(); + } + } + + public void handleSessionTerminateReceived(JinglePayload.Reason reason) { + logger_.fine("\n"); + if (State.Finished.equals(state)) { logger_.warning("Incorrect state: " + state + "\n"); return; } + + stopAll(); + if (State.WaitForTermination.equals(state)) { + waitForRemoteTermination.stop(); + } + if (reason != null && JinglePayload.Reason.Type.Cancel.equals(reason.type)) { + setFinishedState(FileTransfer.State.Type.Canceled, new FileTransferError(FileTransferError.Type.PeerError)); + } + else if (reason != null && JinglePayload.Reason.Type.Decline.equals(reason.type)) { + setFinishedState(FileTransfer.State.Type.Canceled, null); + } + else if (reason != null && JinglePayload.Reason.Type.Success.equals(reason.type)) { + setFinishedState(FileTransfer.State.Type.Finished, null); + } + else { + setFinishedState(FileTransfer.State.Type.Failed, new FileTransferError(FileTransferError.Type.PeerError)); + } + } + + public void handleTransportAcceptReceived(final JingleContentID contentID, JingleTransportPayload transport) { + logger_.fine("\n"); + if (!State.FallbackRequested.equals(state)) { logger_.warning("Incorrect state\n"); return; } + + if (transport instanceof JingleIBBTransportPayload) { + JingleIBBTransportPayload ibbPayload = (JingleIBBTransportPayload)transport; + startTransferring(transporter.createIBBSendSession(ibbPayload.getSessionID(), ( ibbPayload.getBlockSize() != null ? ibbPayload.getBlockSize() : DEFAULT_BLOCK_SIZE), stream)); + } + else { + logger_.fine("Unknown transport replacement\n"); + terminate(JinglePayload.Reason.Type.FailedTransport); + } + } + + public void handleTransportRejectReceived(final JingleContentID contentID, JingleTransportPayload transport) { + logger_.fine("\n"); + + terminate(JinglePayload.Reason.Type.UnsupportedTransports); + } + + protected void startTransferViaRemoteCandidate() { + logger_.fine("\n"); + + if (JingleS5BTransportPayload.Candidate.Type.ProxyType.equals(ourCandidateChoice.type)) { + setState(State.WaitingForPeerProxyActivate); + } + else { + transportSession = createRemoteCandidateSession(); + startTransferringIfCandidateAcknowledged(); + } + } + + protected void startTransferViaLocalCandidate() { + logger_.fine("\n"); + + if (JingleS5BTransportPayload.Candidate.Type.ProxyType.equals(theirCandidateChoice.type)) { + setState(State.WaitingForLocalProxyActivate); + transporter.startActivatingProxy(theirCandidateChoice.jid); + } + else { + transportSession = createLocalCandidateSession(); + startTransferringIfCandidateAcknowledged(); + } + } + + private void startTransferringIfCandidateAcknowledged() { + if (candidateAcknowledged) { + startTransferring(transportSession); + } + else { + setState(State.WaitingForCandidateAcknowledge); + } + } + + protected void handleLocalTransportCandidatesGenerated(final String s5bSessionID, final Vector<JingleS5BTransportPayload.Candidate> candidates, final String dstAddr) { + logger_.fine("\n"); + if (!State.GeneratingInitialLocalCandidates.equals(state)) { logger_.warning("Incorrect state\n"); return; } + + fillCandidateMap(localCandidates, candidates); + + JingleFileTransferDescription description = new JingleFileTransferDescription(); + fileInfo.addHash(new HashElement("sha-1", new ByteArray())); + fileInfo.addHash(new HashElement("md5", new ByteArray())); + description.setFileInfo(fileInfo); + + JingleS5BTransportPayload transport = new JingleS5BTransportPayload(); + transport.setSessionID(s5bSessionID); + transport.setMode(JingleS5BTransportPayload.Mode.TCPMode); + transport.setDstAddr(dstAddr); + for(JingleS5BTransportPayload.Candidate candidate : candidates) { + transport.addCandidate(candidate); + logger_.fine("\t" + "S5B candidate: " + candidate.hostPort.toString() + "\n"); + } + setState(State.WaitingForAccept); + session.sendInitiate(contentID, description, transport); + } + + public void handleTransportInfoAcknowledged(final String id) { + if (id.equals(candidateSelectRequestID)) { + candidateAcknowledged = true; + } + if (State.WaitingForCandidateAcknowledge.equals(state)) { + startTransferring(transportSession); + } + } + + protected JingleContentID getContentID() { + return contentID; + } + + protected void terminate(JinglePayload.Reason.Type reason) { + logger_.fine(reason + "\n"); + + if (!State.Initial.equals(state) && !State.GeneratingInitialLocalCandidates.equals(state) && !State.Finished.equals(state)) { + session.sendTerminate(reason); + } + stopAll(); + setFinishedState(getExternalFinishedState(reason), getFileTransferError(reason)); + } + + protected void fallback() { + if (options.isInBandAllowed()) { + logger_.fine("Trying to fallback to IBB transport.\n"); + JingleIBBTransportPayload ibbTransport = new JingleIBBTransportPayload(); + ibbTransport.setBlockSize(DEFAULT_BLOCK_SIZE); + ibbTransport.setSessionID(idGenerator.generateID()); + setState(State.FallbackRequested); + session.sendTransportReplace(contentID, ibbTransport); + } + else { + logger_.fine("Fallback to IBB transport not allowed.\n"); + terminate(JinglePayload.Reason.Type.ConnectivityError); + } + } + + private void handleTransferFinished(FileTransferError error) { + logger_.fine("\n"); + if (!State.Transferring.equals(state)) { logger_.warning("Incorrect state: " + state + "\n"); return; } + + if (error != null) { + terminate(JinglePayload.Reason.Type.ConnectivityError); + } + else { + sendSessionInfoHash(); + + // wait for other party to terminate session after they have verified the hash + setState(State.WaitForTermination); + waitForRemoteTermination.start(); + } + } + + private void sendSessionInfoHash() { + logger_.fine("\n"); + + JingleFileTransferHash hashElement = new JingleFileTransferHash(); + hashElement.getFileInfo().addHash(new HashElement("sha-1", hashCalculator.getSHA1Hash())); + hashElement.getFileInfo().addHash(new HashElement("md5", hashCalculator.getMD5Hash())); + session.sendInfo(hashElement); + } + + protected void startTransferring(TransportSession transportSession) { + logger_.fine("\n"); + + this.transportSession = transportSession; + processedBytesConnection = transportSession.onBytesSent.connect(onProcessedBytes); + transferFinishedConnection = transportSession.onFinished.connect(new Slot1<FileTransferError>() { + @Override + public void call(FileTransferError e) { + handleTransferFinished(e); + } + }); + setState(State.Transferring); + transportSession.start(); + } + + protected boolean hasPriorityOnCandidateTie() { + return true; + } + + protected boolean isWaitingForPeerProxyActivate() { + return State.WaitingForPeerProxyActivate.equals(state); + } + + protected boolean isWaitingForLocalProxyActivate() { + return State.WaitingForLocalProxyActivate.equals(state); + } + + protected boolean isTryingCandidates() { + return State.TryingCandidates.equals(state); + } + + protected TransportSession createLocalCandidateSession() { + return transporter.createLocalCandidateSession(stream, theirCandidateChoice); + } + + protected TransportSession createRemoteCandidateSession() { + return transporter.createRemoteCandidateSession(stream, ourCandidateChoice); + } + + private void handleWaitForRemoteTerminationTimeout() { + assert(state.equals(State.WaitForTermination)); + logger_.warning("Other party did not terminate session. Terminate it now.\n"); + waitForRemoteTermination.stop(); + terminate(JinglePayload.Reason.Type.MediaError); + } + + private void stopAll() { + logger_.fine(state + "\n"); + switch (state) { + case Initial: logger_.warning("Not yet started\n"); break; + case GeneratingInitialLocalCandidates: transporter.stopGeneratingLocalCandidates(); break; + case WaitingForAccept: break; + case TryingCandidates: transporter.stopTryingRemoteCandidates(); break; + case FallbackRequested: break; + case WaitingForPeerProxyActivate: break; + case WaitingForLocalProxyActivate: transporter.stopActivatingProxy(); break; + case WaitingForCandidateAcknowledge: // Fallthrough + case Transferring: + assert(transportSession != null); + processedBytesConnection.disconnect(); + transferFinishedConnection.disconnect(); + transportSession.stop(); + transportSession = null; + break; + case WaitForTermination: + break; + case Finished: logger_.warning("Already finished\n"); break; + } + if (!State.Initial.equals(state)) { + removeTransporter(); + } + } + + private void setState(State state) { + logger_.fine(state + "\n"); + this.state = state; + onStateChanged.emit(new FileTransfer.State(getExternalState(state))); + } + + private void setFinishedState(FileTransfer.State.Type type, final FileTransferError error) { + logger_.fine("\n"); + this.state = State.Finished; + onStateChanged.emit(new FileTransfer.State(type)); + onFinished.emit(error); + } + + private static FileTransfer.State.Type getExternalState(State state) { + switch (state) { + case Initial: return FileTransfer.State.Type.Initial; + case GeneratingInitialLocalCandidates: return FileTransfer.State.Type.WaitingForStart; + case WaitingForAccept: return FileTransfer.State.Type.WaitingForAccept; + case TryingCandidates: return FileTransfer.State.Type.Negotiating; + case WaitingForPeerProxyActivate: return FileTransfer.State.Type.Negotiating; + case WaitingForLocalProxyActivate: return FileTransfer.State.Type.Negotiating; + case WaitingForCandidateAcknowledge: return FileTransfer.State.Type.Negotiating; + case FallbackRequested: return FileTransfer.State.Type.Negotiating; + case Transferring: return FileTransfer.State.Type.Transferring; + case WaitForTermination: return FileTransfer.State.Type.Transferring; + case Finished: return FileTransfer.State.Type.Finished; + } + assert(false); + return FileTransfer.State.Type.Initial; + } + +}
\ No newline at end of file |