rocks.xmpp.extensions.si.StreamInitiationManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xmpp-extensions-client Show documentation
Show all versions of xmpp-extensions-client Show documentation
Client side business logic for XMPP extensions
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.extensions.si;
import rocks.xmpp.addr.Jid;
import rocks.xmpp.core.session.Manager;
import rocks.xmpp.core.session.XmppSession;
import rocks.xmpp.core.stanza.AbstractIQHandler;
import rocks.xmpp.core.stanza.IQHandler;
import rocks.xmpp.core.stanza.model.IQ;
import rocks.xmpp.core.stanza.model.StanzaError;
import rocks.xmpp.core.stanza.model.errors.Condition;
import rocks.xmpp.extensions.bytestreams.ByteStreamEvent;
import rocks.xmpp.extensions.bytestreams.ByteStreamSession;
import rocks.xmpp.extensions.bytestreams.ibb.InBandByteStreamManager;
import rocks.xmpp.extensions.bytestreams.ibb.model.InBandByteStream;
import rocks.xmpp.extensions.bytestreams.s5b.Socks5ByteStreamManager;
import rocks.xmpp.extensions.bytestreams.s5b.model.Socks5ByteStream;
import rocks.xmpp.extensions.data.model.DataForm;
import rocks.xmpp.extensions.featureneg.model.FeatureNegotiation;
import rocks.xmpp.extensions.filetransfer.FileTransfer;
import rocks.xmpp.extensions.filetransfer.FileTransferManager;
import rocks.xmpp.extensions.filetransfer.FileTransferNegotiator;
import rocks.xmpp.extensions.filetransfer.FileTransferOffer;
import rocks.xmpp.extensions.si.model.StreamInitiation;
import rocks.xmpp.extensions.si.profile.filetransfer.model.SIFileTransferOffer;
import rocks.xmpp.util.concurrent.AsyncResult;
import rocks.xmpp.util.concurrent.CompletionStages;
import java.io.IOException;
import java.io.OutputStream;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* @author Christian Schudt
*/
public final class StreamInitiationManager extends Manager implements FileTransferNegotiator {
private static final Logger logger = Logger.getLogger(StreamInitiationManager.class.getName());
private static final String STREAM_METHOD = "stream-method";
private final Map profileManagers = new ConcurrentHashMap<>();
private final InBandByteStreamManager inBandByteStreamManager;
private final Socks5ByteStreamManager socks5ByteStreamManager;
private final IQHandler iqHandler;
private StreamInitiationManager(final XmppSession xmppSession) {
super(xmppSession);
inBandByteStreamManager = xmppSession.getManager(InBandByteStreamManager.class);
socks5ByteStreamManager = xmppSession.getManager(Socks5ByteStreamManager.class);
// Currently, there's only one profile in XMPP, namely XEP-0096 SI File Transfer.
profileManagers.put(SIFileTransferOffer.NAMESPACE, (iq, streamInitiation) -> {
FileTransferManager fileTransferManager = xmppSession.getManager(FileTransferManager.class);
fileTransferManager.fileTransferOffered(iq, streamInitiation.getId(), streamInitiation.getMimeType(), (FileTransferOffer) streamInitiation.getProfileElement(), streamInitiation, StreamInitiationManager.this);
});
iqHandler = new AbstractIQHandler(IQ.Type.SET) {
@Override
protected IQ processRequest(IQ iq) {
StreamInitiation streamInitiation = iq.getExtension(StreamInitiation.class);
FeatureNegotiation featureNegotiation = streamInitiation.getFeatureNegotiation();
// Assume no valid streams by default, unless valid streams are found.
boolean noValidStreams = true;
if (featureNegotiation != null) {
DataForm dataForm = featureNegotiation.getDataForm();
if (dataForm != null) {
DataForm.Field field = dataForm.findField(STREAM_METHOD);
if (field != null) {
List streamMethods = field.getOptions().stream().map(DataForm.Option::getValue).collect(Collectors.toList());
if (!Collections.disjoint(streamMethods, getSupportedStreamMethods())) {
// Request contains valid streams
noValidStreams = false;
}
}
}
}
if (noValidStreams) {
return iq.createError(new StanzaError(rocks.xmpp.core.stanza.model.errors.Condition.BAD_REQUEST, StreamInitiation.NO_VALID_STREAMS));
} else {
ProfileManager profileManager = profileManagers.get(streamInitiation.getProfile());
if (profileManager == null) {
return iq.createError(new StanzaError(rocks.xmpp.core.stanza.model.errors.Condition.BAD_REQUEST, StreamInitiation.BAD_PROFILE));
} else {
profileManager.handle(iq, streamInitiation);
return null;
}
}
}
};
}
@Override
protected void onEnable() {
super.onEnable();
xmppSession.addIQHandler(StreamInitiation.class, iqHandler);
}
@Override
protected void onDisable() {
super.onDisable();
xmppSession.removeIQHandler(StreamInitiation.class);
}
/**
* Initiates a stream with another entity.
*
* @param receiver The receiver, i.e. the XMPP entity you want to negotiate a stream.
* @param profile The profile. Currently there's only the {@link rocks.xmpp.extensions.si.profile.filetransfer.model.SIFileTransferOffer} profile.
* @param mimeType The mime type of the stream.
* @param timeout The timeout, which wait until the stream has been negotiated.
* @return The async result with the output stream which has been negotiated.
*/
public AsyncResult initiateStream(Jid receiver, SIFileTransferOffer profile, String mimeType, Duration timeout) {
return this.initiateStream(receiver, profile, mimeType, timeout, UUID.randomUUID().toString());
}
/**
* Initiates a stream with another entity.
*
* @param receiver The receiver, i.e. the XMPP entity you want to negotiate a stream.
* @param profile The profile. Currently there's only the {@link rocks.xmpp.extensions.si.profile.filetransfer.model.SIFileTransferOffer} profile.
* @param mimeType The mime type of the stream.
* @param timeout The timeout, which wait until the stream has been negotiated.
* @param sessionId The session id.
* @return The async result with the output stream which has been negotiated.
*/
public AsyncResult initiateStream(Jid receiver, SIFileTransferOffer profile, String mimeType, Duration timeout, String sessionId) {
// Offer stream methods.
List options = getSupportedStreamMethods().stream().map(DataForm.Option::new).collect(Collectors.toList());
DataForm.Field field = DataForm.Field.builder().var(STREAM_METHOD).type(DataForm.Field.Type.LIST_SINGLE).options(options).build();
DataForm dataForm = new DataForm(DataForm.Type.FORM, Collections.singleton(field));
// Offer the file to the recipient and wait until it's accepted.
return xmppSession.query(IQ.set(receiver, new StreamInitiation(Objects.requireNonNull(sessionId), SIFileTransferOffer.NAMESPACE, mimeType, profile, new FeatureNegotiation(dataForm))), timeout).thenCompose(result -> {
// The recipient must response with a stream initiation.
StreamInitiation streamInitiation = result.getExtension(StreamInitiation.class);
FeatureNegotiation featureNegotiation = streamInitiation.getFeatureNegotiation();
// Get the stream method which has been chosen by the recipient.
String streamMethod = featureNegotiation.getDataForm().findField(STREAM_METHOD).getValues().get(0);
CompletionStage byteStreamSessionStage;
// Choose the stream method to be used based on the recipient's choice.
switch (streamMethod) {
case Socks5ByteStream.NAMESPACE:
byteStreamSessionStage = CompletionStages.withFallback(socks5ByteStreamManager.initiateSession(receiver, sessionId), (future, throwable) -> {
// As fallback, if SOCKS5 negotiation failed, try IBB.
logger.log(Level.FINE, "SOCKS5 file transfer failed, falling back to IBB", throwable);
return inBandByteStreamManager.initiateSession(receiver, sessionId, 4096);
}
);
break;
case InBandByteStream.NAMESPACE:
byteStreamSessionStage = inBandByteStreamManager.initiateSession(receiver, sessionId, 4096);
break;
default:
throw new CompletionException(new IOException("Receiver returned unsupported stream method."));
}
return byteStreamSessionStage;
});
}
@Override
public AsyncResult accept(IQ iq, final String sessionId, FileTransferOffer fileTransferOffer, Object
protocol, OutputStream outputStream) {
StreamInitiation streamInitiation = (StreamInitiation) protocol;
DataForm.Field field = streamInitiation.getFeatureNegotiation().getDataForm().findField(STREAM_METHOD);
// These are the offered stream methods by the initiator of the file transfer.
final List offeredStreamMethods = field.getOptions().stream().map(DataForm.Option::getValue).collect(Collectors.toList());
// In the SI response, only include stream methods, which we actually support.
offeredStreamMethods.retainAll(getSupportedStreamMethods());
DataForm.Field fieldReply = DataForm.Field.builder().var(STREAM_METHOD).values(offeredStreamMethods).type(DataForm.Field.Type.LIST_SINGLE).build();
DataForm dataForm = new DataForm(DataForm.Type.SUBMIT, Collections.singleton(fieldReply));
StreamInitiation siResponse = new StreamInitiation(new FeatureNegotiation(dataForm));
CompletableFuture completableFutureS5b = new CompletableFuture<>();
CompletableFuture completableFutureIbb = new CompletableFuture<>();
// Before we reply with the chosen stream method, we
// register a byte stream listener, because we expect the initiator to open a byte stream with us.
Consumer byteStreamListenerS5b = createSessionListener(sessionId, completableFutureS5b);
Consumer byteStreamListenerIbb = createSessionListener(sessionId, completableFutureIbb);
socks5ByteStreamManager.addByteStreamListener(byteStreamListenerS5b);
inBandByteStreamManager.addByteStreamListener(byteStreamListenerIbb);
// Send the stream initiation result.
xmppSession.send(iq.createResult(siResponse));
// Create a stage, which completes when either S5B or IBB negotiation finished, whichever comes first.
CompletionStage eitherS5bOrIbb = completableFutureS5b.applyToEither(completableFutureIbb, Function.identity());
// If any of the previous negotiation failed, always try IBB as fallback.
CompletionStage withFallbackStage = CompletionStages.withFallback(eitherS5bOrIbb, (f, t) -> completableFutureIbb);
// And then wait until the peer opens the stream.
return new AsyncResult<>(withFallbackStage.applyToEither(CompletionStages.timeoutAfter(xmppSession.getConfiguration().getDefaultResponseTimeout().toMillis() * 5, TimeUnit.MILLISECONDS), byteStreamSession -> {
try {
return new FileTransfer(byteStreamSession.getSessionId(), byteStreamSession.getInputStream(), outputStream, fileTransferOffer.getSize());
} catch (IOException e) {
throw new CompletionException(e);
}
}
)).whenComplete((byteStreamSession, throwable) -> {
// Remove the listeners when everything completed.
socks5ByteStreamManager.removeByteStreamListener(byteStreamListenerS5b);
inBandByteStreamManager.removeByteStreamListener(byteStreamListenerIbb);
});
}
private static Consumer createSessionListener(final String sessionId, final CompletableFuture completableFuture) {
return e -> {
if (sessionId.equals(e.getSessionId())) {
// Auto-accept the inbound stream
e.accept().whenComplete((byteStreamSession, throwable) -> {
if (throwable != null) {
completableFuture.completeExceptionally(throwable);
} else {
completableFuture.complete(byteStreamSession);
}
});
}
};
}
@Override
public void reject(IQ iq) {
xmppSession.send(iq.createError(Condition.FORBIDDEN));
}
Collection getSupportedStreamMethods() {
Collection allStreamMethods = new ArrayDeque<>(Arrays.asList(Socks5ByteStream.NAMESPACE, InBandByteStream.NAMESPACE));
allStreamMethods.retainAll(xmppSession.getEnabledFeatures());
return allStreamMethods;
}
private interface ProfileManager {
void handle(IQ iq, StreamInitiation streamInitiation);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy