637 lines
22 KiB
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);
|
|
}
|
|
}
|