Smack/smack-experimental/src/main/java/org/jivesoftware/smackx/jingle_filetransfer/JingleFileTransferSession.java

637 lines
22 KiB
Java

/**
*
* Copyright © 2017 Paul Schaub
*
* 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.jingle_filetransfer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.bytestreams.BytestreamSession;
import org.jivesoftware.smackx.hashes.HashManager;
import org.jivesoftware.smackx.hashes.element.HashElement;
import org.jivesoftware.smackx.jingle.AbstractJingleTransportManager;
import org.jivesoftware.smackx.jingle.JingleManager;
import org.jivesoftware.smackx.jingle.JingleTransportEstablishedCallback;
import org.jivesoftware.smackx.jingle.JingleTransportHandler;
import org.jivesoftware.smackx.jingle.JingleTransportManager;
import org.jivesoftware.smackx.jingle.element.Jingle;
import org.jivesoftware.smackx.jingle.element.JingleAction;
import org.jivesoftware.smackx.jingle.element.JingleContent;
import org.jivesoftware.smackx.jingle.element.JingleContentDescriptionChildElement;
import org.jivesoftware.smackx.jingle.element.JingleContentTransport;
import org.jivesoftware.smackx.jingle.element.JingleReason;
import org.jivesoftware.smackx.jingle.exception.JingleTransportFailureException;
import org.jivesoftware.smackx.jingle.exception.UnsupportedJingleTransportException;
import org.jivesoftware.smackx.jingle_filetransfer.callback.JingleFileTransferCallback;
import org.jivesoftware.smackx.jingle_filetransfer.element.JingleFileTransferChild;
import org.jivesoftware.smackx.jingle_filetransfer.element.JingleFileTransferContentDescription;
import org.jivesoftware.smackx.jingle_ibb.element.JingleIBBTransport;
import org.jxmpp.jid.FullJid;
/**
* JingleSession.
*/
public class JingleFileTransferSession extends AbstractJingleSession {
private static final Logger LOGGER = Logger.getLogger(JingleFileTransferSession.class.getName());
private final File source;
private File target;
private JingleContent proposedContent;
private JingleContent receivedContent;
private JingleTransportHandler<?> transportHandler;
private final FullJid remote;
private final String sessionId;
private final JingleContent.Creator role;
/**
* Send a file.
* @param connection
* @param send
*/
public JingleFileTransferSession(XMPPConnection connection, FullJid recipient, File send) {
super(connection);
this.remote = recipient;
this.source = send;
this.sessionId = StringUtils.randomString(24);
this.role = JingleContent.Creator.initiator;
//Create file content element
JingleFileTransferChild fileTransferChild = fileElementFromFile(send);
JingleContent.Builder cb = JingleContent.getBuilder();
cb.setSenders(JingleContent.Senders.initiator)
.setCreator(JingleContent.Creator.initiator)
.setName(StringUtils.randomString(24))
.setDescription(new JingleFileTransferContentDescription(
Collections.singletonList((JingleContentDescriptionChildElement) fileTransferChild)));
try {
cb.addTransport(defaultTransport());
} catch (Exception e) {
throw new AssertionError("At least IBB should work as a transport method. " + e);
}
this.proposedContent = cb.build();
try {
connection.sendStanza(createFileOffer());
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Could not send session-initiate: " + e, e);
return;
}
AbstractJingleTransportManager<?> tm;
try {
tm = JingleTransportManager.getJingleContentTransportManager(connection,
proposedContent.getJingleTransports().get(0));
} catch (UnsupportedJingleTransportException e) {
throw new AssertionError("Since we initiated the transport, we MUST know the transport method.");
}
transportHandler = tm.createJingleTransportHandler(this);
addTransportInfoListener(transportHandler);
transportHandler.prepareSession();
this.state = new OutgoingInitiated(connection, this);
}
/**
* Receive a file.
* @param connection
*/
public JingleFileTransferSession(XMPPConnection connection, Jingle initiate) {
super(connection);
this.role = JingleContent.Creator.responder;
this.sessionId = initiate.getSessionId();
this.remote = initiate.getInitiator();
this.source = null;
this.receivedContent = initiate.getContents().get(0);
this.state = new IncomingFresh(connection, this);
}
/**
* session-initiate has been sent.
*/
private static class OutgoingInitiated extends AbstractJingleSession {
private final JingleFileTransferSession parent;
public OutgoingInitiated(XMPPConnection connection, JingleFileTransferSession parent) {
super(connection);
this.parent = parent;
}
@Override
protected IQ handleSessionAccept(Jingle jingle) {
parent.receivedContent = jingle.getContents().get(0);
parent.state = new OutgoingAccepted(connection, parent);
//TODO: Notify parent
return IQ.createResultIQ(jingle);
}
@Override
protected IQ handleSessionTerminate(Jingle jingle) {
//TODO: notify client
JingleFileTransferManager.getInstanceFor(connection).removeJingleSession(parent);
return IQ.createResultIQ(jingle);
}
@Override
public JingleManager.FullJidAndSessionId getFullJidAndSessionId() {
return parent.getFullJidAndSessionId();
}
@Override
public JingleContent getReceivedContent() {
return parent.getReceivedContent();
}
@Override
public JingleContent getProposedContent() {
return parent.getProposedContent();
}
@Override
public JingleContent.Creator getRole() {
return parent.getRole();
}
}
private static class OutgoingAccepted extends AbstractJingleSession {
private final JingleFileTransferSession parent;
public OutgoingAccepted(XMPPConnection connection, final JingleFileTransferSession parent) {
super(connection);
this.parent = parent;
parent.transportHandler.establishOutgoingSession(parent.outgoingFileTransferSessionEstablishedCallback);
}
@Override
public JingleManager.FullJidAndSessionId getFullJidAndSessionId() {
return parent.getFullJidAndSessionId();
}
@Override
public JingleContent getReceivedContent() {
return parent.getReceivedContent();
}
@Override
public JingleContent getProposedContent() {
return parent.getProposedContent();
}
@Override
public JingleContent.Creator getRole() {
return parent.getRole();
}
}
private static class IncomingFresh extends AbstractJingleSession {
private final JingleFileTransferSession parent;
public IncomingFresh(XMPPConnection connection, JingleFileTransferSession parent) {
super(connection);
this.parent = parent;
}
@Override
protected IQ handleSessionInitiate(final Jingle initiate) {
if (initiate.getAction() != JingleAction.session_initiate) {
throw new IllegalArgumentException("Jingle action MUST be session-initiate!");
}
parent.receivedContent = initiate.getContents().get(0);
//Get <file/>
JingleFileTransferChild file = (JingleFileTransferChild) parent.receivedContent
.getDescription().getJingleContentDescriptionChildren().get(0);
final JingleFileTransferCallback callback = new JingleFileTransferCallback() {
@Override
public void acceptFileTransfer(File target) throws SmackException.NotConnectedException, InterruptedException {
Jingle response = null;
try {
response = parent.createSessionAccept();
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not create accept-session stanza: " + e, e);
}
connection.sendStanza(response);
parent.proposedContent = response.getContents().get(0);
parent.target = target;
parent.state = new IncomingAccepted(connection, parent);
}
@Override
public void declineFileTransfer() throws SmackException.NotConnectedException, InterruptedException {
connection.sendStanza(parent.createSessionDecline(initiate));
parent.state = null;
JingleFileTransferManager.getInstanceFor(connection).removeJingleSession(parent);
}
};
//Set callback
JingleFileTransferManager.getInstanceFor(connection).notifyIncomingJingleFileTransferListeners(file, callback);
return IQ.createResultIQ(initiate);
}
@Override
public JingleManager.FullJidAndSessionId getFullJidAndSessionId() {
return parent.getFullJidAndSessionId();
}
@Override
public JingleContent getReceivedContent() {
return parent.getReceivedContent();
}
@Override
public JingleContent getProposedContent() {
return parent.getProposedContent();
}
@Override
public JingleContent.Creator getRole() {
return parent.getRole();
}
}
private static class IncomingAccepted extends AbstractJingleSession {
private final JingleFileTransferSession parent;
IncomingAccepted(XMPPConnection connection, JingleFileTransferSession parent) {
super(connection);
this.parent = parent;
AbstractJingleTransportManager<?> tm;
try {
tm = JingleTransportManager.getJingleContentTransportManager(
connection, parent.proposedContent.getJingleTransports().get(0));
} catch (UnsupportedJingleTransportException e) {
throw new AssertionError("Since we accepted the transfer, we MUST know the transport method.");
}
parent.transportHandler = tm.createJingleTransportHandler(this);
parent.addTransportInfoListener(parent.transportHandler);
parent.transportHandler.prepareSession();
parent.transportHandler.establishIncomingSession(parent.incomingFileTransferSessionEstablishedCallback);
}
@Override
public JingleManager.FullJidAndSessionId getFullJidAndSessionId() {
return parent.getFullJidAndSessionId();
}
@Override
public JingleContent getReceivedContent() {
return parent.getReceivedContent();
}
@Override
public JingleContent getProposedContent() {
return parent.getProposedContent();
}
@Override
public JingleContent.Creator getRole() {
return parent.getRole();
}
}
protected Jingle createFileOffer() throws Exception {
Jingle.Builder jb = Jingle.getBuilder();
jb.setSessionId(sessionId)
.setAction(JingleAction.session_initiate)
.setInitiator(connection.getUser());
JingleContent.Builder cb = JingleContent.getBuilder();
cb.setSenders(JingleContent.Senders.initiator)
.setCreator(JingleContent.Creator.initiator)
.setName(StringUtils.randomString(24));
cb.setDescription(new JingleFileTransferContentDescription(Collections.singletonList(
proposedContent.getDescription().getJingleContentDescriptionChildren().get(0))));
cb.addTransport(proposedContent.getJingleTransports().get(0));
jb.addJingleContent(cb.build());
Jingle jingle = jb.build();
jingle.setTo(remote);
jingle.setFrom(connection.getUser());
return jingle;
}
protected Jingle createSessionAccept() throws Exception {
Jingle.Builder jb = Jingle.getBuilder();
jb.setAction(JingleAction.session_accept)
.setResponder(connection.getUser())
.setSessionId(sessionId);
JingleContent.Builder cb = JingleContent.getBuilder();
AbstractJingleTransportManager<?> tm;
try {
tm = JingleTransportManager.getJingleContentTransportManager(
connection, receivedContent.getJingleTransports().get(0));
} catch (UnsupportedJingleTransportException e) {
throw new AssertionError("Should never happen."); //TODO: Make sure.
}
cb.addTransport(tm.createJingleContentTransport(remote))
.setDescription(receivedContent.getDescription())
.setName(receivedContent.getName())
.setCreator(receivedContent.getCreator())
.setSenders(receivedContent.getSenders());
jb.addJingleContent(cb.build());
Jingle accept = jb.build();
accept.setTo(remote);
accept.setFrom(connection.getUser());
return accept;
}
public Jingle createSessionDecline(Jingle initiate) {
Jingle.Builder jb = Jingle.getBuilder();
jb.setAction(JingleAction.session_terminate)
.setReason(JingleReason.Reason.decline)
.setResponder(connection.getUser())
.setSessionId(sessionId);
return jb.build();
}
static JingleFileTransferChild fileElementFromFile(File file) {
JingleFileTransferChild.Builder fb = JingleFileTransferChild.getBuilder();
fb.setFile(file)
.setDescription("A File")
.setMediaType("application/octetStream");
return fb.build();
}
JingleContentTransport defaultTransport() {
JingleTransportManager transportManager = JingleTransportManager.getInstanceFor(connection);
JingleContentTransport transport = null;
Iterator<AbstractJingleTransportManager<?>> iterator =
transportManager.getAvailableJingleBytestreamManagers().iterator();
while (transport == null && iterator.hasNext()) {
AbstractJingleTransportManager<?> tm = iterator.next();
try {
transport = tm.createJingleContentTransport(remote);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Could not create JingleContentTransport " + tm.getNamespace() +
". Skip.");
}
}
return transport;
}
protected JingleTransportEstablishedCallback outgoingFileTransferSessionEstablishedCallback =
new JingleTransportEstablishedCallback() {
@Override
public void onSessionEstablished(BytestreamSession bytestreamSession) {
send(bytestreamSession);
}
@Override
public void onSessionFailure(JingleTransportFailureException reason) {
//TODO: Send transport-replace to fall back to IBB
// Do we already use IBB?
if (transportHandler.getNamespace().equals(JingleIBBTransport.NAMESPACE_V1)) {
//fail
} else {
Jingle.Builder jb = Jingle.getBuilder();
jb.setAction(JingleAction.transport_replace);
}
}
};
protected JingleTransportEstablishedCallback incomingFileTransferSessionEstablishedCallback =
new JingleTransportEstablishedCallback() {
@Override
public void onSessionEstablished(BytestreamSession bytestreamSession) {
read(bytestreamSession);
}
@Override
public void onSessionFailure(JingleTransportFailureException reason) {
LOGGER.log(Level.SEVERE, "SESSION FAILIURE: ", reason);
}
};
void send(BytestreamSession stream) {
if (source == null || !source.exists()) {
throw new IllegalStateException("Source file MUST NOT be null and MUST exist.");
}
byte[] filebuf = new byte[(int) source.length()];
HashElement hashElement = null;
try {
hashElement = FileAndHashReader.readAndCalculateHash(source, filebuf, HashManager.ALGORITHM.SHA_256);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not read file: " + e, e);
//TODO: Terminate session.
return;
}
//TODO: session-info with hash
try {
stream.getOutputStream().write(filebuf);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Caught Exception while sending file: " + e, e);
} finally {
try {
stream.close();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Could not close OutputStream of ByteStream: " + e, e);
}
}
//TODO: session-info to signalize that file has been sent.
}
void read(BytestreamSession session) {
if (target == null) {
throw new IllegalStateException("Target file MUST NOT be null.");
}
//Become mainstream
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
try {
inputStream = session.getInputStream();
fileOutputStream = new FileOutputStream(target);
int size = ((JingleFileTransferChild) proposedContent.getDescription()
.getJingleContentDescriptionChildren().get(0)).getSize();
byte[] filebuf = new byte[size];
byte[] readbuf = new byte[2048];
int read = 0;
while (read < size) {
int r = inputStream.read(readbuf);
if (r >= 0) {
System.arraycopy(readbuf, 0, filebuf, read, r);
read += r;
} else {
//TODO: Terminate?
}
}
fileOutputStream.write(filebuf);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Caught exception while receiving file: " + e, e);
} finally {
try {
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Caught exception while closing FileOutputStream: " + e, e);
}
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Caught exception while closing InputStream: " + e, e);
}
}
}
@Override
public JingleManager.FullJidAndSessionId getFullJidAndSessionId() {
return new JingleManager.FullJidAndSessionId(remote, sessionId);
}
@Override
public JingleContent getProposedContent() {
return proposedContent;
}
public void setProposedContent(JingleContent proposedContent) {
this.proposedContent = proposedContent;
}
@Override
public JingleContent getReceivedContent() {
return receivedContent;
}
public void setReceivedContent(JingleContent receivedContent) {
this.receivedContent = receivedContent;
}
@Override
public JingleContent.Creator getRole() {
return role;
}
@Override
protected IQ handleSessionInitiate(Jingle sessionInitiate) {
return state.handleSessionInitiate(sessionInitiate);
}
@Override
protected IQ handleSessionTerminate(Jingle sessionTerminate) {
return state.handleSessionTerminate(sessionTerminate);
}
@Override
protected IQ handleSessionInfo(Jingle sessionInfo) {
return state.handleSessionInfo(sessionInfo);
}
@Override
protected IQ handleSessionAccept(Jingle sessionAccept) {
return state.handleSessionAccept(sessionAccept);
}
@Override
protected IQ handleContentAdd(Jingle contentAdd) {
return state.handleContentAdd(contentAdd);
}
@Override
protected IQ handleContentAccept(Jingle contentAccept) {
return state.handleContentAccept(contentAccept);
}
@Override
protected IQ handleContentModify(Jingle contentModify) {
return state.handleContentModify(contentModify);
}
@Override
protected IQ handleContentReject(Jingle contentReject) {
return state.handleContentReject(contentReject);
}
@Override
protected IQ handleContentRemove(Jingle contentRemove) {
return state.handleContentRemove(contentRemove);
}
@Override
protected IQ handleDescriptionInfo(Jingle descriptionInfo) {
return state.handleDescriptionInfo(descriptionInfo);
}
@Override
protected IQ handleSecurityInfo(Jingle securityInfo) {
return state.handleSecurityInfo(securityInfo);
}
@Override
protected IQ handleTransportAccept(Jingle transportAccept) {
return state.handleTransportAccept(transportAccept);
}
@Override
protected IQ handleTransportReplace(Jingle transportReplace) {
return state.handleTransportReplace(transportReplace);
}
@Override
protected IQ handleTransportReject(Jingle transportReject) {
return state.handleTransportReject(transportReject);
}
}