refactor Jingle File Transfer. add WebRTCDatachannel transport
This commit is contained in:
parent
8208724172
commit
d3b38a5273
|
@ -41,7 +41,7 @@ public final class Config {
|
|||
|
||||
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US);
|
||||
|
||||
public static final boolean QUICK_LOG = false;
|
||||
public static final boolean QUICK_LOG = true;
|
||||
|
||||
public static final Jid BUG_REPORTS = Jid.of("bugs@conversations.im");
|
||||
public static final Uri HELP = Uri.parse("https://help.conversations.im");
|
||||
|
@ -117,7 +117,7 @@ public final class Config {
|
|||
public static final boolean OMEMO_PADDING = false;
|
||||
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
|
||||
public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true;
|
||||
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
||||
public static final boolean DISABLE_PROXY_LOOKUP = false; //disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback)
|
||||
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
|
||||
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
||||
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
|
||||
|
|
|
@ -62,11 +62,13 @@ import eu.siacs.conversations.xml.Namespace;
|
|||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
|
||||
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
|
||||
import eu.siacs.conversations.xmpp.jingle.DescriptionTransport;
|
||||
import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
|
||||
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
|
||||
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||
import eu.siacs.conversations.xmpp.pep.PublishOptions;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||
|
@ -1262,12 +1264,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
if (Config.REQUIRE_RTP_VERIFICATION) {
|
||||
requireVerification(session);
|
||||
}
|
||||
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
||||
final ImmutableMap.Builder<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
||||
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
|
||||
omemoVerification.setSessionFingerprint(session.getFingerprint());
|
||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
|
||||
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : rtpContentMap.contents.entrySet()) {
|
||||
final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
|
||||
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
|
||||
try {
|
||||
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
|
||||
|
@ -1276,7 +1278,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
descriptionTransportBuilder.put(
|
||||
content.getKey(),
|
||||
new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
|
||||
new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
|
||||
);
|
||||
}
|
||||
return Futures.immediateFuture(
|
||||
|
@ -1296,11 +1298,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
|
||||
final ImmutableMap.Builder<String, RtpContentMap.DescriptionTransport> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
||||
final ImmutableMap.Builder<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> descriptionTransportBuilder = new ImmutableMap.Builder<>();
|
||||
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
|
||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
|
||||
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
|
||||
final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
|
||||
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
|
||||
try {
|
||||
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
|
||||
|
@ -1310,7 +1312,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
omemoVerification.setOrEnsureEqual(decryptedTransport);
|
||||
descriptionTransportBuilder.put(
|
||||
content.getKey(),
|
||||
new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
|
||||
new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
|
||||
);
|
||||
}
|
||||
processPostponed();
|
||||
|
@ -1376,18 +1378,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
));
|
||||
}
|
||||
|
||||
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
if (buildHeader(axolotlMessage, conversation)) {
|
||||
onMessageCreatedCallback.run(axolotlMessage);
|
||||
} else {
|
||||
onMessageCreatedCallback.run(null);
|
||||
}
|
||||
public ListenableFuture<XmppAxolotlMessage> prepareKeyTransportMessage(final Conversation conversation) {
|
||||
return Futures.submit(()->{
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
if (buildHeader(axolotlMessage, conversation)) {
|
||||
return axolotlMessage;
|
||||
} else {
|
||||
throw new IllegalStateException("No session to decrypt to");
|
||||
}
|
||||
});
|
||||
},executor);
|
||||
}
|
||||
|
||||
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
|
||||
|
|
|
@ -27,11 +27,7 @@ public abstract class AbstractGenerator {
|
|||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
||||
private final String[] FEATURES = {
|
||||
Namespace.JINGLE,
|
||||
|
||||
//Jingle File Transfer
|
||||
FileTransferDescription.Version.FT_3.getNamespace(),
|
||||
FileTransferDescription.Version.FT_4.getNamespace(),
|
||||
FileTransferDescription.Version.FT_5.getNamespace(),
|
||||
Namespace.JINGLE_APPS_FILE_TRANSFER,
|
||||
Namespace.JINGLE_TRANSPORTS_S5B,
|
||||
Namespace.JINGLE_TRANSPORTS_IBB,
|
||||
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
|
||||
|
@ -124,6 +120,7 @@ public abstract class AbstractGenerator {
|
|||
if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
|
||||
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
|
||||
features.addAll(Arrays.asList(VOIP_NAMESPACES));
|
||||
features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
|
||||
}
|
||||
if (mXmppConnectionService.broadcastLastActivity()) {
|
||||
features.add(Namespace.IDLE);
|
||||
|
|
|
@ -403,7 +403,12 @@ public class UnifiedPushBroker {
|
|||
updateIntent.putExtra("token", target.instance);
|
||||
updateIntent.putExtra("bytesMessage", payload);
|
||||
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
|
||||
// TODO add distributor verification?
|
||||
final var distributorVerificationIntent = new Intent();
|
||||
distributorVerificationIntent.setPackage(service.getPackageName());
|
||||
final var pendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
|
||||
updateIntent.putExtra("distributor", pendingIntent);
|
||||
service.sendBroadcast(updateIntent);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018, Daniel Gultsch All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation and/or
|
||||
* other materials provided with the distribution.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package eu.siacs.conversations.utils;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class Checksum {
|
||||
|
||||
public static String md5(InputStream inputStream) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
MessageDigest messageDigest;
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("MD5");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
int count;
|
||||
do {
|
||||
count = inputStream.read(buffer);
|
||||
if (count > 0) {
|
||||
messageDigest.update(buffer, 0, count);
|
||||
}
|
||||
} while (count != -1);
|
||||
inputStream.close();
|
||||
return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP);
|
||||
}
|
||||
}
|
|
@ -47,7 +47,11 @@ public final class Namespace {
|
|||
public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1";
|
||||
public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1";
|
||||
public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1";
|
||||
public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1";
|
||||
public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1";
|
||||
public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1";
|
||||
|
||||
public static final String JINGLE_APPS_FILE_TRANSFER = "urn:xmpp:jingle:apps:file-transfer:5";
|
||||
public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0";
|
||||
public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
|
||||
public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
|
||||
|
@ -71,4 +75,5 @@ public final class Namespace {
|
|||
public static final String REPORTING = "urn:xmpp:reporting:1";
|
||||
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
|
||||
public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
|
||||
public static final String HASHES = "urn:xmpp:hashes:2";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractContentMap<
|
||||
D extends GenericDescription, T extends GenericTransportInfo> {
|
||||
|
||||
public final Group group;
|
||||
|
||||
public final Map<String, DescriptionTransport<D, T>> contents;
|
||||
|
||||
protected AbstractContentMap(
|
||||
final Group group, final Map<String, DescriptionTransport<D, T>> contents) {
|
||||
this.group = group;
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
public static class UnsupportedApplicationException extends IllegalArgumentException {
|
||||
UnsupportedApplicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnsupportedTransportException extends IllegalArgumentException {
|
||||
UnsupportedTransportException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Content.Senders> getSenders() {
|
||||
return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
|
||||
}
|
||||
|
||||
public List<String> getNames() {
|
||||
return ImmutableList.copyOf(contents.keySet());
|
||||
}
|
||||
|
||||
JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
|
||||
final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
|
||||
for (final Map.Entry<String, DescriptionTransport<D, T>> entry : this.contents.entrySet()) {
|
||||
final DescriptionTransport<D, T> descriptionTransport = entry.getValue();
|
||||
final Content content =
|
||||
new Content(
|
||||
Content.Creator.INITIATOR,
|
||||
descriptionTransport.senders,
|
||||
entry.getKey());
|
||||
if (descriptionTransport.description != null) {
|
||||
content.addChild(descriptionTransport.description);
|
||||
}
|
||||
content.addChild(descriptionTransport.transport);
|
||||
jinglePacket.addJingleContent(content);
|
||||
}
|
||||
if (this.group != null) {
|
||||
jinglePacket.addGroup(this.group);
|
||||
}
|
||||
return jinglePacket;
|
||||
}
|
||||
|
||||
void requireContentDescriptions() {
|
||||
if (this.contents.size() == 0) {
|
||||
throw new IllegalStateException("No contents available");
|
||||
}
|
||||
for (final Map.Entry<String, DescriptionTransport<D, T>> entry : this.contents.entrySet()) {
|
||||
if (entry.getValue().description == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("%s is lacking content description", entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +1,352 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class AbstractJingleConnection {
|
||||
|
||||
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
|
||||
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
|
||||
|
||||
protected static final List<State> TERMINATED =
|
||||
Arrays.asList(
|
||||
State.ACCEPTED,
|
||||
State.REJECTED,
|
||||
State.REJECTED_RACED,
|
||||
State.RETRACTED,
|
||||
State.RETRACTED_RACED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR,
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR);
|
||||
|
||||
private static final Map<State, Collection<State>> VALID_TRANSITIONS;
|
||||
|
||||
static {
|
||||
final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
|
||||
new ImmutableMap.Builder<>();
|
||||
transitionBuilder.put(
|
||||
State.NULL,
|
||||
ImmutableList.of(
|
||||
State.PROPOSED,
|
||||
State.SESSION_INITIALIZED,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
transitionBuilder.put(
|
||||
State.PROPOSED,
|
||||
ImmutableList.of(
|
||||
State.ACCEPTED,
|
||||
State.PROCEED,
|
||||
State.REJECTED,
|
||||
State.RETRACTED,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
|
||||
// rebinds
|
||||
));
|
||||
transitionBuilder.put(
|
||||
State.PROCEED,
|
||||
ImmutableList.of(
|
||||
State.REJECTED_RACED,
|
||||
State.RETRACTED_RACED,
|
||||
State.SESSION_INITIALIZED_PRE_APPROVED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
|
||||
// bounces of the proceed message
|
||||
));
|
||||
transitionBuilder.put(
|
||||
State.SESSION_INITIALIZED,
|
||||
ImmutableList.of(
|
||||
State.SESSION_ACCEPTED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
|
||||
// and IQ timeouts
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
transitionBuilder.put(
|
||||
State.SESSION_INITIALIZED_PRE_APPROVED,
|
||||
ImmutableList.of(
|
||||
State.SESSION_ACCEPTED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
|
||||
// and IQ timeouts
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
transitionBuilder.put(
|
||||
State.SESSION_ACCEPTED,
|
||||
ImmutableList.of(
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR,
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
VALID_TRANSITIONS = transitionBuilder.build();
|
||||
}
|
||||
|
||||
final JingleConnectionManager jingleConnectionManager;
|
||||
protected final XmppConnectionService xmppConnectionService;
|
||||
protected final Id id;
|
||||
private final Jid initiator;
|
||||
|
||||
AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
|
||||
protected State state = State.NULL;
|
||||
|
||||
AbstractJingleConnection(
|
||||
final JingleConnectionManager jingleConnectionManager,
|
||||
final Id id,
|
||||
final Jid initiator) {
|
||||
this.jingleConnectionManager = jingleConnectionManager;
|
||||
this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
|
||||
this.id = id;
|
||||
this.initiator = initiator;
|
||||
}
|
||||
|
||||
boolean isInitiator() {
|
||||
return initiator.equals(id.account.getJid());
|
||||
}
|
||||
|
||||
abstract void deliverPacket(JinglePacket jinglePacket);
|
||||
|
||||
public Id getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
boolean isInitiator() {
|
||||
return initiator.equals(id.account.getJid());
|
||||
}
|
||||
|
||||
boolean isResponder() {
|
||||
return !initiator.equals(id.account.getJid());
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
protected synchronized boolean isInState(State... state) {
|
||||
return Arrays.asList(state).contains(this.state);
|
||||
}
|
||||
|
||||
protected boolean transition(final State target) {
|
||||
return transition(target, null);
|
||||
}
|
||||
|
||||
protected synchronized boolean transition(final State target, final Runnable runnable) {
|
||||
final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
|
||||
if (validTransitions != null && validTransitions.contains(target)) {
|
||||
this.state = target;
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void transitionOrThrow(final State target) {
|
||||
if (!transition(target)) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Unable to transition from %s to %s", this.state, target));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isTerminated() {
|
||||
return TERMINATED.contains(this.state);
|
||||
}
|
||||
|
||||
abstract void deliverPacket(JinglePacket jinglePacket);
|
||||
|
||||
protected void receiveOutOfOrderAction(
|
||||
final JinglePacket jinglePacket, final JinglePacket.Action action) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
String.format(
|
||||
"%s: received %s even though we are in state %s",
|
||||
id.account.getJid().asBareJid(), action, getState()));
|
||||
if (isTerminated()) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
String.format(
|
||||
"%s: got a reason to terminate with out-of-order. but already in state %s",
|
||||
id.account.getJid().asBareJid(), getState()));
|
||||
respondWithOutOfOrder(jinglePacket);
|
||||
} else {
|
||||
terminateWithOutOfOrder(jinglePacket);
|
||||
}
|
||||
}
|
||||
|
||||
protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid() + ": terminating session with out-of-order");
|
||||
terminateTransport();
|
||||
transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
|
||||
respondWithOutOfOrder(jinglePacket);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
protected void finish() {
|
||||
if (isTerminated()) {
|
||||
this.jingleConnectionManager.finishConnectionOrThrow(this);
|
||||
} else {
|
||||
throw new AssertionError(
|
||||
String.format("Unable to call finish from %s", this.state));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void terminateTransport();
|
||||
|
||||
abstract void notifyRebound();
|
||||
|
||||
protected void sendSessionTerminate(
|
||||
final Reason reason, final String text, final Consumer<State> trigger) {
|
||||
final State previous = this.state;
|
||||
final State target = reasonToState(reason);
|
||||
transitionOrThrow(target);
|
||||
if (previous != State.NULL && trigger != null) {
|
||||
trigger.accept(target);
|
||||
}
|
||||
final JinglePacket jinglePacket =
|
||||
new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
|
||||
jinglePacket.setReason(reason, text);
|
||||
send(jinglePacket);
|
||||
finish();
|
||||
}
|
||||
|
||||
protected void send(final JinglePacket jinglePacket) {
|
||||
jinglePacket.setTo(id.with);
|
||||
xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
|
||||
}
|
||||
|
||||
protected void respondOk(final JinglePacket jinglePacket) {
|
||||
xmppConnectionService.sendIqPacket(
|
||||
id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||
}
|
||||
|
||||
protected void respondWithTieBreak(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
|
||||
}
|
||||
|
||||
protected void respondWithOutOfOrder(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
|
||||
}
|
||||
|
||||
protected void respondWithItemNotFound(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
|
||||
}
|
||||
|
||||
private void respondWithJingleError(
|
||||
final IqPacket original,
|
||||
String jingleCondition,
|
||||
String condition,
|
||||
String conditionType) {
|
||||
jingleConnectionManager.respondWithJingleError(
|
||||
id.account, original, jingleCondition, condition, conditionType);
|
||||
}
|
||||
|
||||
private synchronized void handleIqResponse(final Account account, final IqPacket response) {
|
||||
if (response.getType() == IqPacket.TYPE.ERROR) {
|
||||
handleIqErrorResponse(response);
|
||||
return;
|
||||
}
|
||||
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||
handleIqTimeoutResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleIqErrorResponse(final IqPacket response) {
|
||||
Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
|
||||
final String errorCondition = response.getErrorCondition();
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": received IQ-error from "
|
||||
+ response.getFrom()
|
||||
+ " in RTP session. "
|
||||
+ errorCondition);
|
||||
if (isTerminated()) {
|
||||
Log.i(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.terminateTransport();
|
||||
final State target;
|
||||
if (Arrays.asList(
|
||||
"service-unavailable",
|
||||
"recipient-unavailable",
|
||||
"remote-server-not-found",
|
||||
"remote-server-timeout")
|
||||
.contains(errorCondition)) {
|
||||
target = State.TERMINATED_CONNECTIVITY_ERROR;
|
||||
} else {
|
||||
target = State.TERMINATED_APPLICATION_FAILURE;
|
||||
}
|
||||
transitionOrThrow(target);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
protected void handleIqTimeoutResponse(final IqPacket response) {
|
||||
Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": received IQ timeout in RTP session with "
|
||||
+ id.with
|
||||
+ ". terminating with connectivity error");
|
||||
if (isTerminated()) {
|
||||
Log.i(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.terminateTransport();
|
||||
transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
protected boolean remoteHasFeature(final String feature) {
|
||||
final Contact contact = id.getContact();
|
||||
final Presence presence =
|
||||
contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
|
||||
final ServiceDiscoveryResult serviceDiscoveryResult =
|
||||
presence == null ? null : presence.getServiceDiscoveryResult();
|
||||
final List<String> features =
|
||||
serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
|
||||
return features != null && features.contains(feature);
|
||||
}
|
||||
|
||||
public static class Id implements OngoingRtpSession {
|
||||
public final Account account;
|
||||
|
@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection {
|
|||
return new Id(
|
||||
message.getConversation().getAccount(),
|
||||
message.getCounterpart(),
|
||||
JingleConnectionManager.nextRandomId()
|
||||
);
|
||||
JingleConnectionManager.nextRandomId());
|
||||
}
|
||||
|
||||
public Contact getContact() {
|
||||
|
@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection {
|
|||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Id id = (Id) o;
|
||||
return Objects.equal(account.getUuid(), id.account.getUuid()) &&
|
||||
Objects.equal(with, id.with) &&
|
||||
Objects.equal(sessionId, id.sessionId);
|
||||
return Objects.equal(account.getUuid(), id.account.getUuid())
|
||||
&& Objects.equal(with, id.with)
|
||||
&& Objects.equal(sessionId, id.sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -122,23 +426,36 @@ public abstract class AbstractJingleConnection {
|
|||
}
|
||||
}
|
||||
|
||||
protected static State reasonToState(Reason reason) {
|
||||
return switch (reason) {
|
||||
case SUCCESS -> State.TERMINATED_SUCCESS;
|
||||
case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
|
||||
case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
|
||||
case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
|
||||
case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
|
||||
.TERMINATED_APPLICATION_FAILURE;
|
||||
default -> State.TERMINATED_CONNECTIVITY_ERROR;
|
||||
};
|
||||
}
|
||||
|
||||
public enum State {
|
||||
NULL, //default value; nothing has been sent or received yet
|
||||
NULL, // default value; nothing has been sent or received yet
|
||||
PROPOSED,
|
||||
ACCEPTED,
|
||||
PROCEED,
|
||||
REJECTED,
|
||||
REJECTED_RACED, //used when we want to reject but haven’t received session init yet
|
||||
REJECTED_RACED, // used when we want to reject but haven’t received session init yet
|
||||
RETRACTED,
|
||||
RETRACTED_RACED, //used when receiving a retract after we already asked to proceed
|
||||
SESSION_INITIALIZED, //equal to 'PENDING'
|
||||
RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
|
||||
SESSION_INITIALIZED, // equal to 'PENDING'
|
||||
SESSION_INITIALIZED_PRE_APPROVED,
|
||||
SESSION_ACCEPTED, //equal to 'ACTIVE'
|
||||
TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close
|
||||
TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
|
||||
TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
|
||||
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
|
||||
SESSION_ACCEPTED, // equal to 'ACTIVE'
|
||||
TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
|
||||
TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
|
||||
TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
|
||||
// display retry button)
|
||||
TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
|
||||
// before session was accepted
|
||||
TERMINATED_APPLICATION_FAILURE,
|
||||
TERMINATED_SECURITY_ERROR
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.Collections2;
|
||||
|
@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet;
|
|||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||
|
||||
public final class ContentAddition {
|
||||
|
||||
|
@ -32,12 +36,13 @@ public final class ContentAddition {
|
|||
Collections2.transform(
|
||||
rtpContentMap.contents.entrySet(),
|
||||
e -> {
|
||||
final RtpContentMap.DescriptionTransport dt = e.getValue();
|
||||
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> dt = e.getValue();
|
||||
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("direction", direction)
|
||||
|
@ -77,6 +82,7 @@ public final class ContentAddition {
|
|||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("name", name)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||
|
||||
public class DescriptionTransport<D extends GenericDescription, T extends GenericTransportInfo> {
|
||||
|
||||
public final Content.Senders senders;
|
||||
public final D description;
|
||||
public final T transport;
|
||||
|
||||
public DescriptionTransport(
|
||||
final Content.Senders senders, final D description, final T transport) {
|
||||
this.senders = senders;
|
||||
this.description = description;
|
||||
this.transport = transport;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
|
@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid;
|
|||
|
||||
public class DirectConnectionUtils {
|
||||
|
||||
private static List<InetAddress> getLocalAddresses() {
|
||||
final List<InetAddress> addresses = new ArrayList<>();
|
||||
public static List<InetAddress> getLocalAddresses() {
|
||||
final ImmutableList.Builder<InetAddress> inetAddresses = new ImmutableList.Builder<>();
|
||||
final Enumeration<NetworkInterface> interfaces;
|
||||
try {
|
||||
interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
} catch (SocketException e) {
|
||||
return addresses;
|
||||
} catch (final SocketException e) {
|
||||
return inetAddresses.build();
|
||||
}
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = interfaces.nextElement();
|
||||
|
@ -34,31 +36,15 @@ public class DirectConnectionUtils {
|
|||
if (inetAddress instanceof Inet6Address) {
|
||||
//let's get rid of scope
|
||||
try {
|
||||
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
||||
inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
||||
} catch (UnknownHostException e) {
|
||||
//ignored
|
||||
}
|
||||
} else {
|
||||
addresses.add(inetAddress);
|
||||
inetAddresses.add(inetAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses;
|
||||
return inetAddresses.build();
|
||||
}
|
||||
|
||||
public static List<JingleCandidate> getLocalCandidates(Jid jid) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
ArrayList<JingleCandidate> candidates = new ArrayList<>();
|
||||
for (InetAddress inetAddress : getLocalAddresses()) {
|
||||
final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true);
|
||||
candidate.setHost(inetAddress.getHostAddress());
|
||||
candidate.setPort(random.nextInt(60000) + 1024);
|
||||
candidate.setType(JingleCandidate.TYPE_DIRECT);
|
||||
candidate.setJid(jid);
|
||||
candidate.setPriority(8257536 + candidates.size());
|
||||
candidates.add(candidate);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FileTransferContentMap
|
||||
extends AbstractContentMap<FileTransferDescription, GenericTransportInfo> {
|
||||
|
||||
private static final List<Class<? extends GenericTransportInfo>> SUPPORTED_TRANSPORTS =
|
||||
Arrays.asList(
|
||||
SocksByteStreamsTransportInfo.class,
|
||||
IbbTransportInfo.class,
|
||||
WebRTCDataChannelTransportInfo.class);
|
||||
|
||||
protected FileTransferContentMap(
|
||||
final Group group, final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
|
||||
contents) {
|
||||
super(group, contents);
|
||||
}
|
||||
|
||||
public static FileTransferContentMap of(final JinglePacket jinglePacket) {
|
||||
final Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
|
||||
contents = of(jinglePacket.getJingleContents());
|
||||
return new FileTransferContentMap(jinglePacket.getGroup(), contents);
|
||||
}
|
||||
|
||||
public static DescriptionTransport<FileTransferDescription, GenericTransportInfo> of(
|
||||
final Content content) {
|
||||
final GenericDescription description = content.getDescription();
|
||||
final GenericTransportInfo transportInfo = content.getTransport();
|
||||
final Content.Senders senders = content.getSenders();
|
||||
final FileTransferDescription fileTransferDescription;
|
||||
if (description == null) {
|
||||
fileTransferDescription = null;
|
||||
} else if (description instanceof FileTransferDescription ftDescription) {
|
||||
fileTransferDescription = ftDescription;
|
||||
} else {
|
||||
throw new UnsupportedApplicationException(
|
||||
"Content does not contain file transfer description");
|
||||
}
|
||||
if (!SUPPORTED_TRANSPORTS.contains(transportInfo.getClass())) {
|
||||
throw new UnsupportedTransportException("Content does not have supported transport");
|
||||
}
|
||||
return new DescriptionTransport<>(senders, fileTransferDescription, transportInfo);
|
||||
}
|
||||
|
||||
private static Map<String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
|
||||
of(final Map<String, Content> contents) {
|
||||
return ImmutableMap.copyOf(
|
||||
Maps.transformValues(contents, content -> content == null ? null : of(content)));
|
||||
}
|
||||
|
||||
public static FileTransferContentMap of(
|
||||
final FileTransferDescription.File file, final Transport.InitialTransportInfo initialTransportInfo) {
|
||||
// TODO copy groups
|
||||
final var transportInfo = initialTransportInfo.transportInfo;
|
||||
return new FileTransferContentMap(initialTransportInfo.group,
|
||||
Map.of(
|
||||
initialTransportInfo.contentName,
|
||||
new DescriptionTransport<>(
|
||||
Content.Senders.INITIATOR,
|
||||
FileTransferDescription.of(file),
|
||||
transportInfo)));
|
||||
}
|
||||
|
||||
public FileTransferDescription.File requireOnlyFile() {
|
||||
if (this.contents.size() != 1) {
|
||||
throw new IllegalStateException("Only one file at a time is supported");
|
||||
}
|
||||
final var dt = Iterables.getOnlyElement(this.contents.values());
|
||||
return dt.description.getFile();
|
||||
}
|
||||
|
||||
public FileTransferDescription requireOnlyFileTransferDescription() {
|
||||
if (this.contents.size() != 1) {
|
||||
throw new IllegalStateException("Only one file at a time is supported");
|
||||
}
|
||||
final var dt = Iterables.getOnlyElement(this.contents.values());
|
||||
return dt.description;
|
||||
}
|
||||
|
||||
public GenericTransportInfo requireOnlyTransportInfo() {
|
||||
if (this.contents.size() != 1) {
|
||||
throw new IllegalStateException(
|
||||
"We expect exactly one content with one transport info");
|
||||
}
|
||||
final var dt = Iterables.getOnlyElement(this.contents.values());
|
||||
return dt.transport;
|
||||
}
|
||||
|
||||
public FileTransferContentMap withTransport(final Transport.TransportInfo transportWrapper) {
|
||||
final var transportInfo = transportWrapper.transportInfo;
|
||||
return new FileTransferContentMap(transportWrapper.group,
|
||||
ImmutableMap.copyOf(
|
||||
Maps.transformValues(
|
||||
contents,
|
||||
content -> {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
return new DescriptionTransport<>(
|
||||
content.senders, content.description, transportInfo);
|
||||
})));
|
||||
}
|
||||
|
||||
public FileTransferContentMap candidateUsed(final String streamId, final String cid) {
|
||||
return new FileTransferContentMap(null,
|
||||
ImmutableMap.copyOf(
|
||||
Maps.transformValues(
|
||||
contents,
|
||||
content -> {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
final var transportInfo =
|
||||
new SocksByteStreamsTransportInfo(
|
||||
streamId, Collections.emptyList());
|
||||
final Element candidateUsed =
|
||||
transportInfo.addChild(
|
||||
"candidate-used",
|
||||
Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
candidateUsed.setAttribute("cid", cid);
|
||||
return new DescriptionTransport<>(
|
||||
content.senders, null, transportInfo);
|
||||
})));
|
||||
}
|
||||
|
||||
public FileTransferContentMap candidateError(final String streamId) {
|
||||
return new FileTransferContentMap(null,
|
||||
ImmutableMap.copyOf(
|
||||
Maps.transformValues(
|
||||
contents,
|
||||
content -> {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
final var transportInfo =
|
||||
new SocksByteStreamsTransportInfo(
|
||||
streamId, Collections.emptyList());
|
||||
transportInfo.addChild(
|
||||
"candidate-error", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
return new DescriptionTransport<>(
|
||||
content.senders, null, transportInfo);
|
||||
})));
|
||||
}
|
||||
|
||||
public FileTransferContentMap proxyActivated(final String streamId, final String cid) {
|
||||
return new FileTransferContentMap(null,
|
||||
ImmutableMap.copyOf(
|
||||
Maps.transformValues(
|
||||
contents,
|
||||
content -> {
|
||||
if (content == null) {
|
||||
return null;
|
||||
}
|
||||
final var transportInfo =
|
||||
new SocksByteStreamsTransportInfo(
|
||||
streamId, Collections.emptyList());
|
||||
final Element candidateUsed =
|
||||
transportInfo.addChild(
|
||||
"activated", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
candidateUsed.setAttribute("cid", cid);
|
||||
return new DescriptionTransport<>(
|
||||
content.senders, null, transportInfo);
|
||||
})));
|
||||
}
|
||||
|
||||
FileTransferContentMap transportInfo() {
|
||||
return new FileTransferContentMap(this.group,
|
||||
Maps.transformValues(
|
||||
contents,
|
||||
dt -> new DescriptionTransport<>(dt.senders, null, dt.transport)));
|
||||
}
|
||||
|
||||
FileTransferContentMap transportInfo(
|
||||
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
|
||||
final DescriptionTransport<FileTransferDescription, GenericTransportInfo> descriptionTransport =
|
||||
contents.get(contentName);
|
||||
if (descriptionTransport == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unable to find transport info for content name " + contentName);
|
||||
}
|
||||
final WebRTCDataChannelTransportInfo transportInfo;
|
||||
if (descriptionTransport.transport instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
|
||||
transportInfo = webRTCDataChannelTransportInfo;
|
||||
} else {
|
||||
throw new IllegalStateException("TransportInfo is not WebRTCDataChannel");
|
||||
}
|
||||
final WebRTCDataChannelTransportInfo newTransportInfo = transportInfo.cloneWrapper();
|
||||
newTransportInfo.addCandidate(candidate);
|
||||
return new FileTransferContentMap(
|
||||
null,
|
||||
ImmutableMap.of(
|
||||
contentName,
|
||||
new DescriptionTransport<>(
|
||||
descriptionTransport.senders, null, newTransportInfo)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.IP;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import org.webrtc.PeerConnection;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class IceServers {
|
||||
|
||||
public static List<PeerConnection.IceServer> parse(final IqPacket response) {
|
||||
ImmutableList.Builder<PeerConnection.IceServer> listBuilder = new ImmutableList.Builder<>();
|
||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
final Element services =
|
||||
response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
|
||||
final List<Element> children =
|
||||
services == null ? Collections.emptyList() : services.getChildren();
|
||||
for (final Element child : children) {
|
||||
if ("service".equals(child.getName())) {
|
||||
final String type = child.getAttribute("type");
|
||||
final String host = child.getAttribute("host");
|
||||
final String sport = child.getAttribute("port");
|
||||
final Integer port = sport == null ? null : Ints.tryParse(sport);
|
||||
final String transport = child.getAttribute("transport");
|
||||
final String username = child.getAttribute("username");
|
||||
final String password = child.getAttribute("password");
|
||||
if (Strings.isNullOrEmpty(host) || port == null) {
|
||||
continue;
|
||||
}
|
||||
if (port < 0 || port > 65535) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type)
|
||||
&& Arrays.asList("udp", "tcp").contains(transport)) {
|
||||
if (Arrays.asList("stuns", "turns").contains(type)
|
||||
&& "udp".equals(transport)) {
|
||||
Log.w(
|
||||
Config.LOGTAG,
|
||||
"skipping invalid combination of udp/tls in external services");
|
||||
continue;
|
||||
}
|
||||
|
||||
// STUN URLs do not support a query section since M110
|
||||
final String uri;
|
||||
if (Arrays.asList("stun", "stuns").contains(type)) {
|
||||
uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
|
||||
} else {
|
||||
uri =
|
||||
String.format(
|
||||
"%s:%s:%s?transport=%s",
|
||||
type, IP.wrapIPv6(host), port, transport);
|
||||
}
|
||||
|
||||
final PeerConnection.IceServer.Builder iceServerBuilder =
|
||||
PeerConnection.IceServer.builder(uri);
|
||||
iceServerBuilder.setTlsCertPolicy(
|
||||
PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK);
|
||||
if (username != null && password != null) {
|
||||
iceServerBuilder.setUsername(username);
|
||||
iceServerBuilder.setPassword(password);
|
||||
} else if (Arrays.asList("turn", "turns").contains(type)) {
|
||||
// The WebRTC spec requires throwing an
|
||||
// InvalidAccessError when username (from libwebrtc
|
||||
// source coder)
|
||||
// https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
|
||||
Log.w(
|
||||
Config.LOGTAG,
|
||||
"skipping "
|
||||
+ type
|
||||
+ "/"
|
||||
+ transport
|
||||
+ " without username and password");
|
||||
continue;
|
||||
}
|
||||
final PeerConnection.IceServer iceServer =
|
||||
iceServerBuilder.createIceServer();
|
||||
Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
|
||||
listBuilder.add(iceServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return listBuilder.build();
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.InvalidJid;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
public class JingleCandidate {
|
||||
|
||||
public static int TYPE_UNKNOWN;
|
||||
public static int TYPE_DIRECT = 0;
|
||||
public static int TYPE_PROXY = 1;
|
||||
|
||||
private final boolean ours;
|
||||
private boolean usedByCounterpart = false;
|
||||
private final String cid;
|
||||
private String host;
|
||||
private int port;
|
||||
private int type;
|
||||
private Jid jid;
|
||||
private int priority;
|
||||
|
||||
public JingleCandidate(String cid, boolean ours) {
|
||||
this.ours = ours;
|
||||
this.cid = cid;
|
||||
}
|
||||
|
||||
public String getCid() {
|
||||
return cid;
|
||||
}
|
||||
|
||||
public void setHost(String host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return this.host;
|
||||
}
|
||||
|
||||
public void setJid(final Jid jid) {
|
||||
this.jid = jid;
|
||||
}
|
||||
|
||||
public Jid getJid() {
|
||||
return this.jid;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
if (type == null) {
|
||||
this.type = TYPE_UNKNOWN;
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case "proxy":
|
||||
this.type = TYPE_PROXY;
|
||||
break;
|
||||
case "direct":
|
||||
this.type = TYPE_DIRECT;
|
||||
break;
|
||||
default:
|
||||
this.type = TYPE_UNKNOWN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setPriority(int i) {
|
||||
this.priority = i;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return this.priority;
|
||||
}
|
||||
|
||||
public boolean equals(JingleCandidate other) {
|
||||
return this.getCid().equals(other.getCid());
|
||||
}
|
||||
|
||||
public boolean equalValues(JingleCandidate other) {
|
||||
return other != null && other.getHost().equals(this.getHost()) && (other.getPort() == this.getPort());
|
||||
}
|
||||
|
||||
public boolean isOurs() {
|
||||
return ours;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public static List<JingleCandidate> parse(final List<Element> elements) {
|
||||
final List<JingleCandidate> candidates = new ArrayList<>();
|
||||
for (final Element element : elements) {
|
||||
if ("candidate".equals(element.getName())) {
|
||||
candidates.add(JingleCandidate.parse(element));
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
public static JingleCandidate parse(Element element) {
|
||||
final JingleCandidate candidate = new JingleCandidate(element.getAttribute("cid"), false);
|
||||
candidate.setHost(element.getAttribute("host"));
|
||||
candidate.setJid(InvalidJid.getNullForInvalid(element.getAttributeAsJid("jid")));
|
||||
candidate.setType(element.getAttribute("type"));
|
||||
candidate.setPriority(Integer.parseInt(element.getAttribute("priority")));
|
||||
candidate.setPort(Integer.parseInt(element.getAttribute("port")));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public Element toElement() {
|
||||
Element element = new Element("candidate");
|
||||
element.setAttribute("cid", this.getCid());
|
||||
element.setAttribute("host", this.getHost());
|
||||
element.setAttribute("port", Integer.toString(this.getPort()));
|
||||
if (jid != null) {
|
||||
element.setAttribute("jid", jid);
|
||||
}
|
||||
element.setAttribute("priority", Integer.toString(this.getPriority()));
|
||||
if (this.getType() == TYPE_DIRECT) {
|
||||
element.setAttribute("type", "direct");
|
||||
} else if (this.getType() == TYPE_PROXY) {
|
||||
element.setAttribute("type", "proxy");
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
public void flagAsUsedByCounterpart() {
|
||||
this.usedByCounterpart = true;
|
||||
}
|
||||
|
||||
public boolean isUsedByCounterpart() {
|
||||
return this.usedByCounterpart;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs());
|
||||
}
|
||||
}
|
|
@ -29,10 +29,13 @@ import eu.siacs.conversations.xmpp.XmppConnection;
|
|||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
|
||||
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||
|
||||
|
@ -61,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions =
|
||||
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
|
||||
|
||||
private final HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
|
||||
|
||||
public JingleConnectionManager(XmppConnectionService service) {
|
||||
super(service);
|
||||
this.toneManager = new ToneManager(service);
|
||||
|
@ -90,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
final String descriptionNamespace =
|
||||
content == null ? null : content.getDescriptionNamespace();
|
||||
final AbstractJingleConnection connection;
|
||||
if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
|
||||
if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) {
|
||||
connection = new JingleFileTransferConnection(this, id, from);
|
||||
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
|
||||
&& isUsingClearNet(account)) {
|
||||
|
@ -593,13 +594,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
if (old != null) {
|
||||
old.cancel();
|
||||
}
|
||||
final Account account = message.getConversation().getAccount();
|
||||
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
|
||||
final JingleFileTransferConnection connection =
|
||||
new JingleFileTransferConnection(this, id, account.getJid());
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
|
||||
this.connections.put(id, connection);
|
||||
connection.init(message);
|
||||
new JingleFileTransferConnection(this, message);
|
||||
this.connections.put(connection.getId(), connection);
|
||||
connection.sendSessionInitialize();
|
||||
}
|
||||
|
||||
public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
|
||||
|
@ -658,60 +656,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
return firedUpdates;
|
||||
}
|
||||
|
||||
void getPrimaryCandidate(
|
||||
final Account account,
|
||||
final boolean initiator,
|
||||
final OnPrimaryCandidateFound listener) {
|
||||
if (Config.DISABLE_PROXY_LOOKUP) {
|
||||
listener.onPrimaryCandidateFound(false, null);
|
||||
return;
|
||||
}
|
||||
if (this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
|
||||
listener.onPrimaryCandidateFound(
|
||||
true, this.primaryCandidates.get(account.getJid().asBareJid()));
|
||||
return;
|
||||
}
|
||||
|
||||
final Jid proxy =
|
||||
account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
|
||||
if (proxy == null) {
|
||||
listener.onPrimaryCandidateFound(false, null);
|
||||
return;
|
||||
}
|
||||
final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
|
||||
iq.setTo(proxy);
|
||||
iq.query(Namespace.BYTE_STREAMS);
|
||||
account.getXmppConnection()
|
||||
.sendIqPacket(
|
||||
iq,
|
||||
(a, response) -> {
|
||||
final Element streamhost =
|
||||
response.query()
|
||||
.findChild("streamhost", Namespace.BYTE_STREAMS);
|
||||
final String host =
|
||||
streamhost == null ? null : streamhost.getAttribute("host");
|
||||
final String port =
|
||||
streamhost == null ? null : streamhost.getAttribute("port");
|
||||
if (host != null && port != null) {
|
||||
try {
|
||||
JingleCandidate candidate =
|
||||
new JingleCandidate(nextRandomId(), true);
|
||||
candidate.setHost(host);
|
||||
candidate.setPort(Integer.parseInt(port));
|
||||
candidate.setType(JingleCandidate.TYPE_PROXY);
|
||||
candidate.setJid(proxy);
|
||||
candidate.setPriority(655360 + (initiator ? 30 : 0));
|
||||
primaryCandidates.put(a.getJid().asBareJid(), candidate);
|
||||
listener.onPrimaryCandidateFound(true, candidate);
|
||||
} catch (final NumberFormatException e) {
|
||||
listener.onPrimaryCandidateFound(false, null);
|
||||
}
|
||||
} else {
|
||||
listener.onPrimaryCandidateFound(false, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void retractSessionProposal(final Account account, final Jid with) {
|
||||
synchronized (this.rtpSessionProposals) {
|
||||
RtpSessionProposal matchingProposal = null;
|
||||
|
@ -810,36 +754,53 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
public void deliverIbbPacket(Account account, IqPacket packet) {
|
||||
public void deliverIbbPacket(final Account account, final IqPacket packet) {
|
||||
final String sid;
|
||||
final Element payload;
|
||||
final InbandBytestreamsTransport.PacketType packetType;
|
||||
if (packet.hasChild("open", Namespace.IBB)) {
|
||||
packetType = InbandBytestreamsTransport.PacketType.OPEN;
|
||||
payload = packet.findChild("open", Namespace.IBB);
|
||||
sid = payload.getAttribute("sid");
|
||||
} else if (packet.hasChild("data", Namespace.IBB)) {
|
||||
packetType = InbandBytestreamsTransport.PacketType.DATA;
|
||||
payload = packet.findChild("data", Namespace.IBB);
|
||||
sid = payload.getAttribute("sid");
|
||||
} else if (packet.hasChild("close", Namespace.IBB)) {
|
||||
packetType = InbandBytestreamsTransport.PacketType.CLOSE;
|
||||
payload = packet.findChild("close", Namespace.IBB);
|
||||
sid = payload.getAttribute("sid");
|
||||
} else {
|
||||
packetType = null;
|
||||
payload = null;
|
||||
sid = null;
|
||||
}
|
||||
if (sid != null) {
|
||||
for (final AbstractJingleConnection connection : this.connections.values()) {
|
||||
if (connection instanceof JingleFileTransferConnection fileTransfer) {
|
||||
final JingleTransport transport = fileTransfer.getTransport();
|
||||
if (transport instanceof JingleInBandTransport inBandTransport) {
|
||||
if (inBandTransport.matches(account, sid)) {
|
||||
inBandTransport.deliverPayload(packet, payload);
|
||||
if (sid == null) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid");
|
||||
account.getXmppConnection()
|
||||
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
|
||||
return;
|
||||
}
|
||||
for (final AbstractJingleConnection connection : this.connections.values()) {
|
||||
if (connection instanceof JingleFileTransferConnection fileTransfer) {
|
||||
final Transport transport = fileTransfer.getTransport();
|
||||
if (transport instanceof InbandBytestreamsTransport inBandTransport) {
|
||||
if (sid.equals(inBandTransport.getStreamId())) {
|
||||
if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) {
|
||||
account.getXmppConnection()
|
||||
.sendIqPacket(
|
||||
packet.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||
} else {
|
||||
account.getXmppConnection()
|
||||
.sendIqPacket(
|
||||
packet.generateResponse(IqPacket.TYPE.ERROR), null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet);
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid);
|
||||
account.getXmppConnection()
|
||||
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,265 +0,0 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
public class JingleInBandTransport extends JingleTransport {
|
||||
|
||||
private final Account account;
|
||||
private final Jid counterpart;
|
||||
private final int blockSize;
|
||||
private int seq = 0;
|
||||
private final String sessionId;
|
||||
|
||||
private boolean established = false;
|
||||
|
||||
private boolean connected = true;
|
||||
|
||||
private DownloadableFile file;
|
||||
private final JingleFileTransferConnection connection;
|
||||
|
||||
private InputStream fileInputStream = null;
|
||||
private InputStream innerInputStream = null;
|
||||
private OutputStream fileOutputStream = null;
|
||||
private long remainingSize = 0;
|
||||
private long fileSize = 0;
|
||||
private MessageDigest digest;
|
||||
|
||||
private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
|
||||
|
||||
private final OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
|
||||
@Override
|
||||
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
if (packet.getType() == IqPacket.TYPE.RESULT) {
|
||||
if (remainingSize > 0) {
|
||||
sendNextBlock();
|
||||
}
|
||||
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
|
||||
onFileTransmissionStatusChanged.onFileTransferAborted();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) {
|
||||
this.connection = connection;
|
||||
this.account = connection.getId().account;
|
||||
this.counterpart = connection.getId().with;
|
||||
this.blockSize = blockSize;
|
||||
this.sessionId = sid;
|
||||
}
|
||||
|
||||
private void sendClose() {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending ibb close");
|
||||
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||
iq.setTo(this.counterpart);
|
||||
Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
|
||||
close.setAttribute("sid", this.sessionId);
|
||||
this.account.getXmppConnection().sendIqPacket(iq, null);
|
||||
}
|
||||
|
||||
public boolean matches(final Account account, final String sessionId) {
|
||||
return this.account == account && this.sessionId.equals(sessionId);
|
||||
}
|
||||
|
||||
public void connect(final OnTransportConnected callback) {
|
||||
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||
iq.setTo(this.counterpart);
|
||||
Element open = iq.addChild("open", "http://jabber.org/protocol/ibb");
|
||||
open.setAttribute("sid", this.sessionId);
|
||||
open.setAttribute("stanza", "iq");
|
||||
open.setAttribute("block-size", Integer.toString(this.blockSize));
|
||||
this.connected = true;
|
||||
this.account.getXmppConnection().sendIqPacket(iq, (account, packet) -> {
|
||||
if (packet.getType() != IqPacket.TYPE.RESULT) {
|
||||
callback.failed();
|
||||
} else {
|
||||
callback.established();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
|
||||
this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
|
||||
this.file = file;
|
||||
try {
|
||||
this.digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.reset();
|
||||
this.fileOutputStream = connection.getFileOutputStream();
|
||||
if (this.fileOutputStream == null) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not create output stream");
|
||||
callback.onFileTransferAborted();
|
||||
return;
|
||||
}
|
||||
this.remainingSize = this.fileSize = file.getExpectedSize();
|
||||
} catch (final NoSuchAlgorithmException | IOException e) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " " + e.getMessage());
|
||||
callback.onFileTransferAborted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) {
|
||||
this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback);
|
||||
this.file = file;
|
||||
try {
|
||||
this.remainingSize = this.file.getExpectedSize();
|
||||
this.fileSize = this.remainingSize;
|
||||
this.digest = MessageDigest.getInstance("SHA-1");
|
||||
this.digest.reset();
|
||||
fileInputStream = connection.getFileInputStream();
|
||||
if (fileInputStream == null) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could no create input stream");
|
||||
callback.onFileTransferAborted();
|
||||
return;
|
||||
}
|
||||
innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
|
||||
if (this.connected) {
|
||||
this.sendNextBlock();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.onFileTransferAborted();
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
this.connected = false;
|
||||
FileBackend.close(fileOutputStream);
|
||||
FileBackend.close(fileInputStream);
|
||||
}
|
||||
|
||||
private void sendNextBlock() {
|
||||
byte[] buffer = new byte[this.blockSize];
|
||||
try {
|
||||
int count = innerInputStream.read(buffer);
|
||||
if (count == -1) {
|
||||
sendClose();
|
||||
file.setSha1Sum(digest.digest());
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sendNextBlock() count was -1");
|
||||
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
|
||||
fileInputStream.close();
|
||||
return;
|
||||
} else if (count != buffer.length) {
|
||||
int rem = innerInputStream.read(buffer, count, buffer.length - count);
|
||||
if (rem > 0) {
|
||||
count += rem;
|
||||
}
|
||||
}
|
||||
this.remainingSize -= count;
|
||||
this.digest.update(buffer, 0, count);
|
||||
String base64 = Base64.encodeToString(buffer, 0, count, Base64.NO_WRAP);
|
||||
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||
iq.setTo(this.counterpart);
|
||||
Element data = iq.addChild("data", "http://jabber.org/protocol/ibb");
|
||||
data.setAttribute("seq", Integer.toString(this.seq));
|
||||
data.setAttribute("block-size", Integer.toString(this.blockSize));
|
||||
data.setAttribute("sid", this.sessionId);
|
||||
data.setContent(base64);
|
||||
this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived);
|
||||
this.account.getXmppConnection().r(); //don't fill up stanza queue too much
|
||||
this.seq++;
|
||||
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
|
||||
if (this.remainingSize <= 0) {
|
||||
file.setSha1Sum(digest.digest());
|
||||
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
|
||||
sendClose();
|
||||
fileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": io exception during sendNextBlock() " + e.getMessage());
|
||||
FileBackend.close(fileInputStream);
|
||||
this.onFileTransmissionStatusChanged.onFileTransferAborted();
|
||||
}
|
||||
}
|
||||
|
||||
private void receiveNextBlock(String data) {
|
||||
try {
|
||||
byte[] buffer = Base64.decode(data, Base64.NO_WRAP);
|
||||
if (this.remainingSize < buffer.length) {
|
||||
buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize);
|
||||
}
|
||||
this.remainingSize -= buffer.length;
|
||||
this.fileOutputStream.write(buffer);
|
||||
this.digest.update(buffer);
|
||||
if (this.remainingSize <= 0) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received last block. waiting for close");
|
||||
} else {
|
||||
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage(), e);
|
||||
FileBackend.close(fileOutputStream);
|
||||
this.onFileTransmissionStatusChanged.onFileTransferAborted();
|
||||
}
|
||||
}
|
||||
|
||||
private void done() {
|
||||
try {
|
||||
file.setSha1Sum(digest.digest());
|
||||
fileOutputStream.flush();
|
||||
fileOutputStream.close();
|
||||
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
|
||||
FileBackend.close(fileOutputStream);
|
||||
this.onFileTransmissionStatusChanged.onFileTransferAborted();
|
||||
}
|
||||
}
|
||||
|
||||
void deliverPayload(IqPacket packet, Element payload) {
|
||||
if (payload.getName().equals("open")) {
|
||||
if (!established) {
|
||||
established = true;
|
||||
connected = true;
|
||||
this.receiveNextBlock("");
|
||||
this.account.getXmppConnection().sendIqPacket(
|
||||
packet.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||
} else {
|
||||
this.account.getXmppConnection().sendIqPacket(
|
||||
packet.generateResponse(IqPacket.TYPE.ERROR), null);
|
||||
}
|
||||
} else if (connected && payload.getName().equals("data")) {
|
||||
this.receiveNextBlock(payload.getContent());
|
||||
this.account.getXmppConnection().sendIqPacket(
|
||||
packet.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||
} else if (connected && payload.getName().equals("close")) {
|
||||
this.connected = false;
|
||||
this.account.getXmppConnection().sendIqPacket(
|
||||
packet.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||
if (this.remainingSize <= 0) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close. done");
|
||||
done();
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ibb close with " + this.remainingSize + " remaining");
|
||||
FileBackend.close(fileOutputStream);
|
||||
this.onFileTransmissionStatusChanged.onFileTransferAborted();
|
||||
}
|
||||
} else {
|
||||
this.account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ import com.google.common.base.Strings;
|
|||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
@ -30,14 +29,10 @@ import eu.siacs.conversations.Config;
|
|||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Conversational;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||
import eu.siacs.conversations.utils.IP;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
@ -78,96 +73,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
Arrays.asList(
|
||||
State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
|
||||
private static final long BUSY_TIME_OUT = 30;
|
||||
private static final List<State> TERMINATED =
|
||||
Arrays.asList(
|
||||
State.ACCEPTED,
|
||||
State.REJECTED,
|
||||
State.REJECTED_RACED,
|
||||
State.RETRACTED,
|
||||
State.RETRACTED_RACED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR,
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR);
|
||||
|
||||
private static final Map<State, Collection<State>> VALID_TRANSITIONS;
|
||||
|
||||
static {
|
||||
final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
|
||||
new ImmutableMap.Builder<>();
|
||||
transitionBuilder.put(
|
||||
State.NULL,
|
||||
ImmutableList.of(
|
||||
State.PROPOSED,
|
||||
State.SESSION_INITIALIZED,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
transitionBuilder.put(
|
||||
State.PROPOSED,
|
||||
ImmutableList.of(
|
||||
State.ACCEPTED,
|
||||
State.PROCEED,
|
||||
State.REJECTED,
|
||||
State.RETRACTED,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
|
||||
// rebinds
|
||||
));
|
||||
transitionBuilder.put(
|
||||
State.PROCEED,
|
||||
ImmutableList.of(
|
||||
State.REJECTED_RACED,
|
||||
State.RETRACTED_RACED,
|
||||
State.SESSION_INITIALIZED_PRE_APPROVED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
|
||||
// bounces of the proceed message
|
||||
));
|
||||
transitionBuilder.put(
|
||||
State.SESSION_INITIALIZED,
|
||||
ImmutableList.of(
|
||||
State.SESSION_ACCEPTED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
|
||||
// and IQ timeouts
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
transitionBuilder.put(
|
||||
State.SESSION_INITIALIZED_PRE_APPROVED,
|
||||
ImmutableList.of(
|
||||
State.SESSION_ACCEPTED,
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
|
||||
// and IQ timeouts
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
transitionBuilder.put(
|
||||
State.SESSION_ACCEPTED,
|
||||
ImmutableList.of(
|
||||
State.TERMINATED_SUCCESS,
|
||||
State.TERMINATED_DECLINED_OR_BUSY,
|
||||
State.TERMINATED_CONNECTIVITY_ERROR,
|
||||
State.TERMINATED_CANCEL_OR_TIMEOUT,
|
||||
State.TERMINATED_APPLICATION_FAILURE,
|
||||
State.TERMINATED_SECURITY_ERROR));
|
||||
VALID_TRANSITIONS = transitionBuilder.build();
|
||||
}
|
||||
|
||||
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
|
||||
private final Queue<Map.Entry<String, RtpContentMap.DescriptionTransport>>
|
||||
private final Queue<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>>
|
||||
pendingIceCandidates = new LinkedList<>();
|
||||
private final OmemoVerification omemoVerification = new OmemoVerification();
|
||||
private final Message message;
|
||||
private State state = State.NULL;
|
||||
|
||||
private Set<Media> proposedMedia;
|
||||
private RtpContentMap initiatorRtpContentMap;
|
||||
private RtpContentMap responderRtpContentMap;
|
||||
|
@ -192,18 +104,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
id.sessionId);
|
||||
}
|
||||
|
||||
private static State reasonToState(Reason reason) {
|
||||
return switch (reason) {
|
||||
case SUCCESS -> State.TERMINATED_SUCCESS;
|
||||
case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
|
||||
case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
|
||||
case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
|
||||
case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
|
||||
.TERMINATED_APPLICATION_FAILURE;
|
||||
default -> State.TERMINATED_CONNECTIVITY_ERROR;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
synchronized void deliverPacket(final JinglePacket jinglePacket) {
|
||||
switch (jinglePacket.getAction()) {
|
||||
|
@ -233,7 +133,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
return;
|
||||
}
|
||||
webRTCWrapper.close();
|
||||
if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
|
||||
if (isResponder() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
|
||||
xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
|
||||
}
|
||||
if (isInState(
|
||||
|
@ -322,7 +222,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
|
||||
private void receiveTransportInfo(
|
||||
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
|
||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> candidates =
|
||||
final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> candidates =
|
||||
contentMap.contents.entrySet();
|
||||
final RtpContentMap remote = getRemoteContentMap();
|
||||
final Set<String> remoteContentIds =
|
||||
|
@ -522,7 +422,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
|
||||
setRemoteContentMap(modifiedContentMap);
|
||||
|
||||
final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator());
|
||||
final SessionDescription answer = SessionDescription.of(modifiedContentMap, isResponder());
|
||||
|
||||
final org.webrtc.SessionDescription sdp =
|
||||
new org.webrtc.SessionDescription(
|
||||
|
@ -596,7 +496,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
final SessionDescription offer;
|
||||
try {
|
||||
offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator());
|
||||
offer = SessionDescription.of(modifiedRemoteContentMap, isResponder());
|
||||
} catch (final IllegalArgumentException | NullPointerException e) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
|
@ -815,7 +715,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
final RtpContentMap nextRemote =
|
||||
currentRemote.addContent(
|
||||
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
|
||||
return SessionDescription.of(nextRemote, !isInitiator());
|
||||
return SessionDescription.of(nextRemote, isResponder());
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Unexpected rollback condition. Senders were not uniformly none");
|
||||
|
@ -881,7 +781,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
|
||||
final SessionDescription offer;
|
||||
try {
|
||||
offer = SessionDescription.of(modifiedContentMap, !isInitiator());
|
||||
offer = SessionDescription.of(modifiedContentMap, isResponder());
|
||||
} catch (final IllegalArgumentException | NullPointerException e) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
|
@ -1066,7 +966,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
final boolean isOffer)
|
||||
throws ExecutionException, InterruptedException {
|
||||
final SessionDescription sessionDescription =
|
||||
SessionDescription.of(restartContentMap, !isInitiator());
|
||||
SessionDescription.of(restartContentMap, isResponder());
|
||||
final org.webrtc.SessionDescription.Type type =
|
||||
isOffer
|
||||
? org.webrtc.SessionDescription.Type.OFFER
|
||||
|
@ -1095,14 +995,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
|
||||
private void processCandidates(
|
||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
|
||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
|
||||
final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> contents) {
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : contents) {
|
||||
processCandidate(content);
|
||||
}
|
||||
}
|
||||
|
||||
private void processCandidate(
|
||||
final Map.Entry<String, RtpContentMap.DescriptionTransport> content) {
|
||||
final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content) {
|
||||
final RtpContentMap rtpContentMap = getRemoteContentMap();
|
||||
final List<String> indices = toIdentificationTags(rtpContentMap);
|
||||
final String sdpMid = content.getKey(); // aka content name
|
||||
|
@ -1204,21 +1104,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
|
||||
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
|
||||
if (isInitiator()) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
String.format(
|
||||
"%s: received session-initiate even though we were initiating",
|
||||
id.account.getJid().asBareJid()));
|
||||
if (isTerminated()) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
String.format(
|
||||
"%s: got a reason to terminate with out-of-order. but already in state %s",
|
||||
id.account.getJid().asBareJid(), getState()));
|
||||
respondWithOutOfOrder(jinglePacket);
|
||||
} else {
|
||||
terminateWithOutOfOrder(jinglePacket);
|
||||
}
|
||||
receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
|
||||
return;
|
||||
}
|
||||
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
|
||||
|
@ -1300,13 +1186,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
|
||||
private void receiveSessionAccept(final JinglePacket jinglePacket) {
|
||||
if (!isInitiator()) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
String.format(
|
||||
"%s: received session-accept even though we were responding",
|
||||
id.account.getJid().asBareJid()));
|
||||
terminateWithOutOfOrder(jinglePacket);
|
||||
if (isResponder()) {
|
||||
receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
|
||||
return;
|
||||
}
|
||||
final ListenableFuture<RtpContentMap> future =
|
||||
|
@ -1491,7 +1372,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
|
||||
private void addIceCandidatesFromBlackLog() {
|
||||
Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
|
||||
Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> foo;
|
||||
while ((foo = this.pendingIceCandidates.poll()) != null) {
|
||||
processCandidate(foo);
|
||||
Log.d(
|
||||
|
@ -2061,24 +1942,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void sendSessionTerminate(final Reason reason) {
|
||||
protected void sendSessionTerminate(final Reason reason) {
|
||||
sendSessionTerminate(reason, null);
|
||||
}
|
||||
|
||||
private void sendSessionTerminate(final Reason reason, final String text) {
|
||||
final State previous = this.state;
|
||||
final State target = reasonToState(reason);
|
||||
transitionOrThrow(target);
|
||||
if (previous != State.NULL) {
|
||||
writeLogMessage(target);
|
||||
}
|
||||
final JinglePacket jinglePacket =
|
||||
new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
|
||||
jinglePacket.setReason(reason, text);
|
||||
send(jinglePacket);
|
||||
finish();
|
||||
|
||||
protected void sendSessionTerminate(final Reason reason, final String text) {
|
||||
sendSessionTerminate(reason,text, this::writeLogMessage);
|
||||
}
|
||||
|
||||
|
||||
private void sendTransportInfo(
|
||||
final String contentName, IceUdpTransportInfo.Candidate candidate) {
|
||||
final RtpContentMap transportInfo;
|
||||
|
@ -2099,110 +1972,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
send(jinglePacket);
|
||||
}
|
||||
|
||||
private void send(final JinglePacket jinglePacket) {
|
||||
jinglePacket.setTo(id.with);
|
||||
xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
|
||||
}
|
||||
|
||||
private synchronized void handleIqResponse(final Account account, final IqPacket response) {
|
||||
if (response.getType() == IqPacket.TYPE.ERROR) {
|
||||
handleIqErrorResponse(response);
|
||||
return;
|
||||
}
|
||||
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||
handleIqTimeoutResponse(response);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleIqErrorResponse(final IqPacket response) {
|
||||
Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
|
||||
final String errorCondition = response.getErrorCondition();
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": received IQ-error from "
|
||||
+ response.getFrom()
|
||||
+ " in RTP session. "
|
||||
+ errorCondition);
|
||||
if (isTerminated()) {
|
||||
Log.i(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.webRTCWrapper.close();
|
||||
final State target;
|
||||
if (Arrays.asList(
|
||||
"service-unavailable",
|
||||
"recipient-unavailable",
|
||||
"remote-server-not-found",
|
||||
"remote-server-timeout")
|
||||
.contains(errorCondition)) {
|
||||
target = State.TERMINATED_CONNECTIVITY_ERROR;
|
||||
} else {
|
||||
target = State.TERMINATED_APPLICATION_FAILURE;
|
||||
}
|
||||
transitionOrThrow(target);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
private void handleIqTimeoutResponse(final IqPacket response) {
|
||||
Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": received IQ timeout in RTP session with "
|
||||
+ id.with
|
||||
+ ". terminating with connectivity error");
|
||||
if (isTerminated()) {
|
||||
Log.i(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": ignoring error because session was already terminated");
|
||||
return;
|
||||
}
|
||||
this.webRTCWrapper.close();
|
||||
transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid() + ": terminating session with out-of-order");
|
||||
this.webRTCWrapper.close();
|
||||
transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
|
||||
respondWithOutOfOrder(jinglePacket);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
private void respondWithTieBreak(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
|
||||
}
|
||||
|
||||
private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
|
||||
}
|
||||
|
||||
private void respondWithItemNotFound(final JinglePacket jinglePacket) {
|
||||
respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
|
||||
}
|
||||
|
||||
void respondWithJingleError(
|
||||
final IqPacket original,
|
||||
String jingleCondition,
|
||||
String condition,
|
||||
String conditionType) {
|
||||
jingleConnectionManager.respondWithJingleError(
|
||||
id.account, original, jingleCondition, condition, conditionType);
|
||||
}
|
||||
|
||||
private void respondOk(final JinglePacket jinglePacket) {
|
||||
xmppConnectionService.sendIqPacket(
|
||||
id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||
}
|
||||
|
||||
public RtpEndUserState getEndUserState() {
|
||||
switch (this.state) {
|
||||
case NULL, PROPOSED, SESSION_INITIALIZED -> {
|
||||
|
@ -2398,7 +2167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
+ ": received endCall() when session has already been terminated. nothing to do");
|
||||
return;
|
||||
}
|
||||
if (isInState(State.PROPOSED) && !isInitiator()) {
|
||||
if (isInState(State.PROPOSED) && isResponder()) {
|
||||
rejectCallFromProposed();
|
||||
return;
|
||||
}
|
||||
|
@ -2527,22 +2296,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
sendSessionAccept();
|
||||
}
|
||||
|
||||
private synchronized boolean isInState(State... state) {
|
||||
return Arrays.asList(state).contains(this.state);
|
||||
}
|
||||
|
||||
private boolean transition(final State target) {
|
||||
return transition(target, null);
|
||||
}
|
||||
|
||||
private synchronized boolean transition(final State target, final Runnable runnable) {
|
||||
final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
|
||||
if (validTransitions != null && validTransitions.contains(target)) {
|
||||
this.state = target;
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
|
||||
@Override
|
||||
protected synchronized boolean transition(final State target, final Runnable runnable) {
|
||||
if (super.transition(target, runnable)) {
|
||||
updateEndUserState();
|
||||
updateOngoingCallNotification();
|
||||
return true;
|
||||
|
@ -2551,13 +2308,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
}
|
||||
|
||||
void transitionOrThrow(final State target) {
|
||||
if (!transition(target)) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Unable to transition from %s to %s", this.state, target));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(final IceCandidate iceCandidate) {
|
||||
final RtpContentMap rtpContentMap =
|
||||
|
@ -2893,98 +2643,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
id.account,
|
||||
request,
|
||||
(account, response) -> {
|
||||
ImmutableList.Builder<PeerConnection.IceServer> listBuilder =
|
||||
new ImmutableList.Builder<>();
|
||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
final Element services =
|
||||
response.findChild(
|
||||
"services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
|
||||
final List<Element> children =
|
||||
services == null
|
||||
? Collections.emptyList()
|
||||
: services.getChildren();
|
||||
for (final Element child : children) {
|
||||
if ("service".equals(child.getName())) {
|
||||
final String type = child.getAttribute("type");
|
||||
final String host = child.getAttribute("host");
|
||||
final String sport = child.getAttribute("port");
|
||||
final Integer port =
|
||||
sport == null ? null : Ints.tryParse(sport);
|
||||
final String transport = child.getAttribute("transport");
|
||||
final String username = child.getAttribute("username");
|
||||
final String password = child.getAttribute("password");
|
||||
if (Strings.isNullOrEmpty(host) || port == null) {
|
||||
continue;
|
||||
}
|
||||
if (port < 0 || port > 65535) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Arrays.asList("stun", "stuns", "turn", "turns")
|
||||
.contains(type)
|
||||
&& Arrays.asList("udp", "tcp").contains(transport)) {
|
||||
if (Arrays.asList("stuns", "turns").contains(type)
|
||||
&& "udp".equals(transport)) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": skipping invalid combination of udp/tls in external services");
|
||||
continue;
|
||||
}
|
||||
|
||||
// STUN URLs do not support a query section since M110
|
||||
final String uri;
|
||||
if (Arrays.asList("stun", "stuns").contains(type)) {
|
||||
uri =
|
||||
String.format(
|
||||
"%s:%s:%s",
|
||||
type, IP.wrapIPv6(host), port);
|
||||
} else {
|
||||
uri =
|
||||
String.format(
|
||||
"%s:%s:%s?transport=%s",
|
||||
type,
|
||||
IP.wrapIPv6(host),
|
||||
port,
|
||||
transport);
|
||||
}
|
||||
|
||||
final PeerConnection.IceServer.Builder iceServerBuilder =
|
||||
PeerConnection.IceServer.builder(uri);
|
||||
iceServerBuilder.setTlsCertPolicy(
|
||||
PeerConnection.TlsCertPolicy
|
||||
.TLS_CERT_POLICY_INSECURE_NO_CHECK);
|
||||
if (username != null && password != null) {
|
||||
iceServerBuilder.setUsername(username);
|
||||
iceServerBuilder.setPassword(password);
|
||||
} else if (Arrays.asList("turn", "turns").contains(type)) {
|
||||
// The WebRTC spec requires throwing an
|
||||
// InvalidAccessError when username (from libwebrtc
|
||||
// source coder)
|
||||
// https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": skipping "
|
||||
+ type
|
||||
+ "/"
|
||||
+ transport
|
||||
+ " without username and password");
|
||||
continue;
|
||||
}
|
||||
final PeerConnection.IceServer iceServer =
|
||||
iceServerBuilder.createIceServer();
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
id.account.getJid().asBareJid()
|
||||
+ ": discovered ICE Server: "
|
||||
+ iceServer);
|
||||
listBuilder.add(iceServer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final List<PeerConnection.IceServer> iceServers = listBuilder.build();
|
||||
final var iceServers = IceServers.parse(response);
|
||||
if (iceServers.size() == 0) {
|
||||
Log.w(
|
||||
Config.LOGTAG,
|
||||
|
@ -3002,12 +2661,18 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
@Override
|
||||
protected void terminateTransport() {
|
||||
this.webRTCWrapper.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finish() {
|
||||
if (isTerminated()) {
|
||||
this.cancelRingingTimeout();
|
||||
this.webRTCWrapper.verifyClosed();
|
||||
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
|
||||
this.jingleConnectionManager.finishConnectionOrThrow(this);
|
||||
super.finish();
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
String.format("Unable to call finish from %s", this.state));
|
||||
|
@ -3045,14 +2710,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
}
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
boolean isTerminated() {
|
||||
return TERMINATED.contains(this.state);
|
||||
}
|
||||
|
||||
public Optional<VideoTrack> getLocalVideoTrack() {
|
||||
return webRTCWrapper.getLocalVideoTrack();
|
||||
}
|
||||
|
@ -3091,17 +2748,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
|||
return remoteHasFeature(Namespace.SDP_OFFER_ANSWER);
|
||||
}
|
||||
|
||||
private boolean remoteHasFeature(final String feature) {
|
||||
final Contact contact = id.getContact();
|
||||
final Presence presence =
|
||||
contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
|
||||
final ServiceDiscoveryResult serviceDiscoveryResult =
|
||||
presence == null ? null : presence.getServiceDiscoveryResult();
|
||||
final List<String> features =
|
||||
serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
|
||||
return features != null && features.contains(feature);
|
||||
}
|
||||
|
||||
private interface OnIceServersDiscovered {
|
||||
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
|
||||
}
|
||||
|
|
|
@ -1,305 +0,0 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.SocksSocketFactory;
|
||||
import eu.siacs.conversations.utils.WakeLockHelper;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
|
||||
|
||||
public class JingleSocks5Transport extends JingleTransport {
|
||||
|
||||
private static final int SOCKET_TIMEOUT_DIRECT = 3000;
|
||||
private static final int SOCKET_TIMEOUT_PROXY = 5000;
|
||||
|
||||
private final JingleCandidate candidate;
|
||||
private final JingleFileTransferConnection connection;
|
||||
private final String destination;
|
||||
private final Account account;
|
||||
private OutputStream outputStream;
|
||||
private InputStream inputStream;
|
||||
private boolean isEstablished = false;
|
||||
private boolean activated = false;
|
||||
private ServerSocket serverSocket;
|
||||
private Socket socket;
|
||||
|
||||
JingleSocks5Transport(JingleFileTransferConnection jingleConnection, JingleCandidate candidate) {
|
||||
final MessageDigest messageDigest;
|
||||
try {
|
||||
messageDigest = MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
this.candidate = candidate;
|
||||
this.connection = jingleConnection;
|
||||
this.account = jingleConnection.getId().account;
|
||||
final StringBuilder destBuilder = new StringBuilder();
|
||||
if (this.connection.getFtVersion() == FileTransferDescription.Version.FT_3) {
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination");
|
||||
destBuilder.append(this.connection.getId().sessionId);
|
||||
} else {
|
||||
destBuilder.append(this.connection.getTransportId());
|
||||
}
|
||||
if (candidate.isOurs()) {
|
||||
destBuilder.append(this.account.getJid());
|
||||
destBuilder.append(this.connection.getId().with);
|
||||
} else {
|
||||
destBuilder.append(this.connection.getId().with);
|
||||
destBuilder.append(this.account.getJid());
|
||||
}
|
||||
messageDigest.reset();
|
||||
this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes()));
|
||||
if (candidate.isOurs() && candidate.getType() == JingleCandidate.TYPE_DIRECT) {
|
||||
createServerSocket();
|
||||
}
|
||||
}
|
||||
|
||||
private void createServerSocket() {
|
||||
try {
|
||||
serverSocket = new ServerSocket();
|
||||
serverSocket.bind(new InetSocketAddress(InetAddress.getByName(candidate.getHost()), candidate.getPort()));
|
||||
new Thread(() -> {
|
||||
try {
|
||||
final Socket socket = serverSocket.accept();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
acceptIncomingSocketConnection(socket);
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to read from socket", e);
|
||||
|
||||
}
|
||||
}).start();
|
||||
} catch (IOException e) {
|
||||
if (!serverSocket.isClosed()) {
|
||||
Log.d(Config.LOGTAG, "unable to accept socket", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to bind server socket ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptIncomingSocketConnection(final Socket socket) throws IOException {
|
||||
Log.d(Config.LOGTAG, "accepted connection from " + socket.getInetAddress().getHostAddress());
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT_DIRECT);
|
||||
final byte[] authBegin = new byte[2];
|
||||
final InputStream inputStream = socket.getInputStream();
|
||||
final OutputStream outputStream = socket.getOutputStream();
|
||||
ByteStreams.readFully(inputStream, authBegin);
|
||||
if (authBegin[0] != 0x5) {
|
||||
socket.close();
|
||||
}
|
||||
final short methodCount = authBegin[1];
|
||||
final byte[] methods = new byte[methodCount];
|
||||
ByteStreams.readFully(inputStream, methods);
|
||||
if (SocksSocketFactory.contains((byte) 0x00, methods)) {
|
||||
outputStream.write(new byte[]{0x05, 0x00});
|
||||
} else {
|
||||
outputStream.write(new byte[]{0x05, (byte) 0xff});
|
||||
}
|
||||
final byte[] connectCommand = new byte[4];
|
||||
ByteStreams.readFully(inputStream, connectCommand);
|
||||
if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) {
|
||||
int destinationCount = inputStream.read();
|
||||
final byte[] destination = new byte[destinationCount];
|
||||
ByteStreams.readFully(inputStream, destination);
|
||||
final byte[] port = new byte[2];
|
||||
ByteStreams.readFully(inputStream, port);
|
||||
final String receivedDestination = new String(destination);
|
||||
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
|
||||
final byte[] responseHeader;
|
||||
final boolean success;
|
||||
if (receivedDestination.equals(this.destination) && this.socket == null) {
|
||||
responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03};
|
||||
success = true;
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": destination mismatch. received " + receivedDestination + " (expected " + this.destination + ")");
|
||||
responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03};
|
||||
success = false;
|
||||
}
|
||||
response.put(responseHeader);
|
||||
response.put((byte) destination.length);
|
||||
response.put(destination);
|
||||
response.put(port);
|
||||
outputStream.write(response.array());
|
||||
outputStream.flush();
|
||||
if (success) {
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": successfully processed connection to candidate " + candidate.getHost() + ":" + candidate.getPort());
|
||||
socket.setSoTimeout(0);
|
||||
this.socket = socket;
|
||||
this.inputStream = inputStream;
|
||||
this.outputStream = outputStream;
|
||||
this.isEstablished = true;
|
||||
FileBackend.close(serverSocket);
|
||||
} else {
|
||||
FileBackend.close(socket);
|
||||
}
|
||||
} else {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void connect(final OnTransportConnected callback) {
|
||||
new Thread(() -> {
|
||||
final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY;
|
||||
try {
|
||||
final boolean useTor = this.account.isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect();
|
||||
if (useTor) {
|
||||
socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort());
|
||||
} else {
|
||||
socket = new Socket();
|
||||
SocketAddress address = new InetSocketAddress(candidate.getHost(), candidate.getPort());
|
||||
socket.connect(address, timeout);
|
||||
}
|
||||
inputStream = socket.getInputStream();
|
||||
outputStream = socket.getOutputStream();
|
||||
socket.setSoTimeout(timeout);
|
||||
SocksSocketFactory.createSocksConnection(socket, destination, 0);
|
||||
socket.setSoTimeout(0);
|
||||
isEstablished = true;
|
||||
callback.established();
|
||||
} catch (final IOException e) {
|
||||
callback.failed();
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
|
||||
public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
|
||||
new Thread(() -> {
|
||||
InputStream fileInputStream = null;
|
||||
final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getId().sessionId);
|
||||
long transmitted = 0;
|
||||
try {
|
||||
wakeLock.acquire();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.reset();
|
||||
fileInputStream = connection.getFileInputStream();
|
||||
if (fileInputStream == null) {
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create input stream");
|
||||
callback.onFileTransferAborted();
|
||||
return;
|
||||
}
|
||||
final InputStream innerInputStream = AbstractConnectionManager.upgrade(file, fileInputStream);
|
||||
long size = file.getExpectedSize();
|
||||
int count;
|
||||
byte[] buffer = new byte[8192];
|
||||
while ((count = innerInputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, count);
|
||||
digest.update(buffer, 0, count);
|
||||
transmitted += count;
|
||||
connection.updateProgress((int) ((((double) transmitted) / size) * 100));
|
||||
}
|
||||
outputStream.flush();
|
||||
file.setSha1Sum(digest.digest());
|
||||
if (callback != null) {
|
||||
callback.onFileTransmitted(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
final Account account = this.account;
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed sending file after " + transmitted + "/" + file.getExpectedSize() + " (" + socket.getInetAddress() + ":" + socket.getPort() + ")", e);
|
||||
callback.onFileTransferAborted();
|
||||
} finally {
|
||||
FileBackend.close(fileInputStream);
|
||||
WakeLockHelper.release(wakeLock);
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
|
||||
public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
|
||||
new Thread(() -> {
|
||||
OutputStream fileOutputStream = null;
|
||||
final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getId().sessionId);
|
||||
try {
|
||||
wakeLock.acquire();
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.reset();
|
||||
//inputStream.skip(45);
|
||||
socket.setSoTimeout(30000);
|
||||
fileOutputStream = connection.getFileOutputStream();
|
||||
if (fileOutputStream == null) {
|
||||
callback.onFileTransferAborted();
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create output stream");
|
||||
return;
|
||||
}
|
||||
double size = file.getExpectedSize();
|
||||
long remainingSize = file.getExpectedSize();
|
||||
byte[] buffer = new byte[8192];
|
||||
int count;
|
||||
while (remainingSize > 0) {
|
||||
count = inputStream.read(buffer);
|
||||
if (count == -1) {
|
||||
callback.onFileTransferAborted();
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining");
|
||||
return;
|
||||
} else {
|
||||
fileOutputStream.write(buffer, 0, count);
|
||||
digest.update(buffer, 0, count);
|
||||
remainingSize -= count;
|
||||
}
|
||||
connection.updateProgress((int) (((size - remainingSize) / size) * 100));
|
||||
}
|
||||
fileOutputStream.flush();
|
||||
fileOutputStream.close();
|
||||
file.setSha1Sum(digest.digest());
|
||||
callback.onFileTransmitted(file);
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": " + e.getMessage());
|
||||
callback.onFileTransferAborted();
|
||||
} finally {
|
||||
WakeLockHelper.release(wakeLock);
|
||||
FileBackend.close(fileOutputStream);
|
||||
FileBackend.close(inputStream);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public boolean isProxy() {
|
||||
return this.candidate.getType() == JingleCandidate.TYPE_PROXY;
|
||||
}
|
||||
|
||||
public boolean needsActivation() {
|
||||
return (this.isProxy() && !this.activated);
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
FileBackend.close(inputStream);
|
||||
FileBackend.close(outputStream);
|
||||
FileBackend.close(socket);
|
||||
FileBackend.close(serverSocket);
|
||||
}
|
||||
|
||||
public boolean isEstablished() {
|
||||
return this.isEstablished;
|
||||
}
|
||||
|
||||
public JingleCandidate getCandidate() {
|
||||
return this.candidate;
|
||||
}
|
||||
|
||||
public void setActivated(boolean activated) {
|
||||
this.activated = activated;
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
|
||||
public abstract class JingleTransport {
|
||||
public abstract void connect(final OnTransportConnected callback);
|
||||
|
||||
public abstract void receive(final DownloadableFile file,
|
||||
final OnFileTransmissionStatusChanged callback);
|
||||
|
||||
public abstract void send(final DownloadableFile file,
|
||||
final OnFileTransmissionStatusChanged callback);
|
||||
|
||||
public abstract void disconnect();
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -8,9 +9,9 @@ public class MediaBuilder {
|
|||
private String media;
|
||||
private int port;
|
||||
private String protocol;
|
||||
private List<Integer> formats;
|
||||
private String format;
|
||||
private String connectionData;
|
||||
private ArrayListMultimap<String,String> attributes;
|
||||
private Multimap<String, String> attributes;
|
||||
|
||||
public MediaBuilder setMedia(String media) {
|
||||
this.media = media;
|
||||
|
@ -27,8 +28,13 @@ public class MediaBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public MediaBuilder setFormats(List<Integer> formats) {
|
||||
this.formats = formats;
|
||||
public MediaBuilder setFormats(final List<Integer> formats) {
|
||||
this.format = Joiner.on(' ').join(formats);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MediaBuilder setFormat(final String format) {
|
||||
this.format = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -37,12 +43,13 @@ public class MediaBuilder {
|
|||
return this;
|
||||
}
|
||||
|
||||
public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
|
||||
public MediaBuilder setAttributes(Multimap<String, String> attributes) {
|
||||
this.attributes = attributes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SessionDescription.Media createMedia() {
|
||||
return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes);
|
||||
return new SessionDescription.Media(
|
||||
media, port, protocol, format, connectionData, attributes);
|
||||
}
|
||||
}
|
|
@ -3,12 +3,14 @@ package eu.siacs.conversations.xmpp.jingle;
|
|||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||
|
||||
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
|
||||
public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
||||
public OmemoVerifiedRtpContentMap(Group group, Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
|
||||
super(group, contents);
|
||||
for(final DescriptionTransport descriptionTransport : contents.values()) {
|
||||
for(final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport : contents.values()) {
|
||||
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
||||
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
|
||||
continue;
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package eu.siacs.conversations.xmpp.jingle;
|
||||
|
||||
public interface OnPrimaryCandidateFound {
|
||||
void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
|
||||
}
|
|
@ -6,7 +6,6 @@ import com.google.common.base.Preconditions;
|
|||
import com.google.common.base.Predicates;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
@ -31,19 +30,17 @@ import java.util.Set;
|
|||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class RtpContentMap {
|
||||
public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
|
||||
|
||||
public final Group group;
|
||||
public final Map<String, DescriptionTransport> contents;
|
||||
|
||||
public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
||||
this.group = group;
|
||||
this.contents = contents;
|
||||
public RtpContentMap(
|
||||
Group group,
|
||||
Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
|
||||
super(group, contents);
|
||||
}
|
||||
|
||||
public static RtpContentMap of(final JinglePacket jinglePacket) {
|
||||
final Map<String, DescriptionTransport> contents =
|
||||
DescriptionTransport.of(jinglePacket.getJingleContents());
|
||||
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents =
|
||||
of(jinglePacket.getJingleContents());
|
||||
if (isOmemoVerified(contents)) {
|
||||
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
|
||||
} else {
|
||||
|
@ -51,12 +48,15 @@ public class RtpContentMap {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
|
||||
final Collection<DescriptionTransport> values = contents.values();
|
||||
private static boolean isOmemoVerified(
|
||||
Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
|
||||
final Collection<DescriptionTransport<RtpDescription, IceUdpTransportInfo>> values =
|
||||
contents.values();
|
||||
if (values.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
for (final DescriptionTransport descriptionTransport : values) {
|
||||
for (final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport :
|
||||
values) {
|
||||
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
||||
continue;
|
||||
}
|
||||
|
@ -67,13 +67,13 @@ public class RtpContentMap {
|
|||
|
||||
public static RtpContentMap of(
|
||||
final SessionDescription sessionDescription, final boolean isInitiator) {
|
||||
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
||||
new ImmutableMap.Builder<>();
|
||||
final ImmutableMap.Builder<
|
||||
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
contentMapBuilder = new ImmutableMap.Builder<>();
|
||||
for (SessionDescription.Media media : sessionDescription.media) {
|
||||
final String id = Iterables.getFirst(media.attributes.get("mid"), null);
|
||||
Preconditions.checkNotNull(id, "media has no mid");
|
||||
contentMapBuilder.put(
|
||||
id, DescriptionTransport.of(sessionDescription, isInitiator, media));
|
||||
contentMapBuilder.put(id, of(sessionDescription, isInitiator, media));
|
||||
}
|
||||
final String groupAttribute =
|
||||
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
|
||||
|
@ -94,26 +94,6 @@ public class RtpContentMap {
|
|||
}));
|
||||
}
|
||||
|
||||
public Set<Content.Senders> getSenders() {
|
||||
return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
|
||||
}
|
||||
|
||||
public List<String> getNames() {
|
||||
return ImmutableList.copyOf(contents.keySet());
|
||||
}
|
||||
|
||||
void requireContentDescriptions() {
|
||||
if (this.contents.size() == 0) {
|
||||
throw new IllegalStateException("No contents available");
|
||||
}
|
||||
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
||||
if (entry.getValue().description == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("%s is lacking content description", entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requireDTLSFingerprint() {
|
||||
requireDTLSFingerprint(false);
|
||||
}
|
||||
|
@ -122,7 +102,8 @@ public class RtpContentMap {
|
|||
if (this.contents.size() == 0) {
|
||||
throw new IllegalStateException("No contents available");
|
||||
}
|
||||
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
||||
for (Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> entry :
|
||||
this.contents.entrySet()) {
|
||||
final IceUdpTransportInfo transport = entry.getValue().transport;
|
||||
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
||||
if (fingerprint == null
|
||||
|
@ -146,31 +127,10 @@ public class RtpContentMap {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
|
||||
final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
|
||||
if (this.group != null) {
|
||||
jinglePacket.addGroup(this.group);
|
||||
}
|
||||
for (Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
||||
final DescriptionTransport descriptionTransport = entry.getValue();
|
||||
final Content content =
|
||||
new Content(
|
||||
Content.Creator.INITIATOR,
|
||||
descriptionTransport.senders,
|
||||
entry.getKey());
|
||||
if (descriptionTransport.description != null) {
|
||||
content.addChild(descriptionTransport.description);
|
||||
}
|
||||
content.addChild(descriptionTransport.transport);
|
||||
jinglePacket.addJingleContent(content);
|
||||
}
|
||||
return jinglePacket;
|
||||
}
|
||||
|
||||
RtpContentMap transportInfo(
|
||||
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
|
||||
final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
|
||||
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
|
||||
contents.get(contentName);
|
||||
final IceUdpTransportInfo transportInfo =
|
||||
descriptionTransport == null ? null : descriptionTransport.transport;
|
||||
if (transportInfo == null) {
|
||||
|
@ -183,7 +143,7 @@ public class RtpContentMap {
|
|||
null,
|
||||
ImmutableMap.of(
|
||||
contentName,
|
||||
new DescriptionTransport(
|
||||
new DescriptionTransport<>(
|
||||
descriptionTransport.senders, null, newTransportInfo)));
|
||||
}
|
||||
|
||||
|
@ -193,21 +153,24 @@ public class RtpContentMap {
|
|||
Maps.transformValues(
|
||||
contents,
|
||||
dt ->
|
||||
new DescriptionTransport(
|
||||
new DescriptionTransport<>(
|
||||
dt.senders, null, dt.transport.cloneWrapper())));
|
||||
}
|
||||
|
||||
RtpContentMap withCandidates(
|
||||
ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) {
|
||||
final ImmutableMap.Builder<String, DescriptionTransport> contentBuilder =
|
||||
new ImmutableMap.Builder<>();
|
||||
for (final Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
||||
final ImmutableMap.Builder<
|
||||
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
contentBuilder = new ImmutableMap.Builder<>();
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
entry : this.contents.entrySet()) {
|
||||
final String name = entry.getKey();
|
||||
final DescriptionTransport descriptionTransport = entry.getValue();
|
||||
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
|
||||
entry.getValue();
|
||||
final var transport = descriptionTransport.transport;
|
||||
contentBuilder.put(
|
||||
name,
|
||||
new DescriptionTransport(
|
||||
new DescriptionTransport<>(
|
||||
descriptionTransport.senders,
|
||||
descriptionTransport.description,
|
||||
transport.withCandidates(candidates.get(name))));
|
||||
|
@ -247,7 +210,7 @@ public class RtpContentMap {
|
|||
}
|
||||
|
||||
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
|
||||
final DescriptionTransport descriptionTransport = this.contents.get(contentName);
|
||||
final var descriptionTransport = this.contents.get(contentName);
|
||||
if (descriptionTransport == null) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
|
@ -287,7 +250,7 @@ public class RtpContentMap {
|
|||
|
||||
public boolean emptyCandidates() {
|
||||
int count = 0;
|
||||
for (DescriptionTransport descriptionTransport : contents.values()) {
|
||||
for (final var descriptionTransport : contents.values()) {
|
||||
count += descriptionTransport.transport.getCandidates().size();
|
||||
}
|
||||
return count == 0;
|
||||
|
@ -300,17 +263,19 @@ public class RtpContentMap {
|
|||
|
||||
public RtpContentMap modifiedCredentials(
|
||||
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
|
||||
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
||||
new ImmutableMap.Builder<>();
|
||||
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
|
||||
final DescriptionTransport descriptionTransport = content.getValue();
|
||||
final ImmutableMap.Builder<
|
||||
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
contentMapBuilder = new ImmutableMap.Builder<>();
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
content : contents.entrySet()) {
|
||||
final var descriptionTransport = content.getValue();
|
||||
final RtpDescription rtpDescription = descriptionTransport.description;
|
||||
final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
|
||||
final IceUdpTransportInfo modifiedTransportInfo =
|
||||
transportInfo.modifyCredentials(credentials, setup);
|
||||
contentMapBuilder.put(
|
||||
content.getKey(),
|
||||
new DescriptionTransport(
|
||||
new DescriptionTransport<>(
|
||||
descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
|
||||
}
|
||||
return new RtpContentMap(this.group, contentMapBuilder.build());
|
||||
|
@ -321,16 +286,18 @@ public class RtpContentMap {
|
|||
this.group,
|
||||
Maps.transformValues(
|
||||
contents,
|
||||
dt -> new DescriptionTransport(senders, dt.description, dt.transport)));
|
||||
dt -> new DescriptionTransport<>(senders, dt.description, dt.transport)));
|
||||
}
|
||||
|
||||
public RtpContentMap modifiedSendersChecked(
|
||||
final boolean isInitiator, final Map<String, Content.Senders> modification) {
|
||||
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
||||
new ImmutableMap.Builder<>();
|
||||
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
|
||||
final ImmutableMap.Builder<
|
||||
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
contentMapBuilder = new ImmutableMap.Builder<>();
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
content : contents.entrySet()) {
|
||||
final String id = content.getKey();
|
||||
final DescriptionTransport descriptionTransport = content.getValue();
|
||||
final var descriptionTransport = content.getValue();
|
||||
final Content.Senders currentSenders = descriptionTransport.senders;
|
||||
final Content.Senders targetSenders = modification.get(id);
|
||||
if (targetSenders == null || currentSenders == targetSenders) {
|
||||
|
@ -339,7 +306,7 @@ public class RtpContentMap {
|
|||
checkSenderModification(isInitiator, currentSenders, targetSenders);
|
||||
contentMapBuilder.put(
|
||||
id,
|
||||
new DescriptionTransport(
|
||||
new DescriptionTransport<>(
|
||||
targetSenders,
|
||||
descriptionTransport.description,
|
||||
descriptionTransport.transport));
|
||||
|
@ -386,7 +353,7 @@ public class RtpContentMap {
|
|||
Maps.transformValues(
|
||||
this.contents,
|
||||
dt ->
|
||||
new DescriptionTransport(
|
||||
new DescriptionTransport<>(
|
||||
dt.senders,
|
||||
RtpDescription.stub(dt.description.getMedia()),
|
||||
IceUdpTransportInfo.STUB)));
|
||||
|
@ -415,120 +382,96 @@ public class RtpContentMap {
|
|||
|
||||
public RtpContentMap addContent(
|
||||
final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
|
||||
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
|
||||
final Map<String, DescriptionTransport> combinedFixedTransport =
|
||||
Maps.transformValues(
|
||||
combined,
|
||||
dt -> {
|
||||
final IceUdpTransportInfo iceUdpTransportInfo;
|
||||
if (dt.transport.isStub()) {
|
||||
final IceUdpTransportInfo.Credentials credentials =
|
||||
getDistinctCredentials();
|
||||
final Collection<String> iceOptions = getCombinedIceOptions();
|
||||
final DTLS dtls = getDistinctDtls();
|
||||
iceUdpTransportInfo =
|
||||
IceUdpTransportInfo.of(
|
||||
credentials,
|
||||
iceOptions,
|
||||
setupOverwrite,
|
||||
dtls.hash,
|
||||
dtls.fingerprint);
|
||||
} else {
|
||||
final IceUdpTransportInfo.Fingerprint fp =
|
||||
dt.transport.getFingerprint();
|
||||
final IceUdpTransportInfo.Setup setup = fp.getSetup();
|
||||
iceUdpTransportInfo =
|
||||
IceUdpTransportInfo.of(
|
||||
dt.transport.getCredentials(),
|
||||
dt.transport.getIceOptions(),
|
||||
setup == IceUdpTransportInfo.Setup.ACTPASS
|
||||
? setupOverwrite
|
||||
: setup,
|
||||
fp.getHash(),
|
||||
fp.getContent());
|
||||
}
|
||||
return new DescriptionTransport(
|
||||
dt.senders, dt.description, iceUdpTransportInfo);
|
||||
});
|
||||
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
|
||||
merge(contents, modification.contents);
|
||||
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
combinedFixedTransport =
|
||||
Maps.transformValues(
|
||||
combined,
|
||||
dt -> {
|
||||
final IceUdpTransportInfo iceUdpTransportInfo;
|
||||
if (dt.transport.isStub()) {
|
||||
final IceUdpTransportInfo.Credentials credentials =
|
||||
getDistinctCredentials();
|
||||
final Collection<String> iceOptions =
|
||||
getCombinedIceOptions();
|
||||
final DTLS dtls = getDistinctDtls();
|
||||
iceUdpTransportInfo =
|
||||
IceUdpTransportInfo.of(
|
||||
credentials,
|
||||
iceOptions,
|
||||
setupOverwrite,
|
||||
dtls.hash,
|
||||
dtls.fingerprint);
|
||||
} else {
|
||||
final IceUdpTransportInfo.Fingerprint fp =
|
||||
dt.transport.getFingerprint();
|
||||
final IceUdpTransportInfo.Setup setup = fp.getSetup();
|
||||
iceUdpTransportInfo =
|
||||
IceUdpTransportInfo.of(
|
||||
dt.transport.getCredentials(),
|
||||
dt.transport.getIceOptions(),
|
||||
setup == IceUdpTransportInfo.Setup.ACTPASS
|
||||
? setupOverwrite
|
||||
: setup,
|
||||
fp.getHash(),
|
||||
fp.getContent());
|
||||
}
|
||||
return new DescriptionTransport<>(
|
||||
dt.senders, dt.description, iceUdpTransportInfo);
|
||||
});
|
||||
return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
|
||||
}
|
||||
|
||||
private static Map<String, DescriptionTransport> merge(
|
||||
final Map<String, DescriptionTransport> a, final Map<String, DescriptionTransport> b) {
|
||||
final Map<String, DescriptionTransport> combined = new LinkedHashMap<>();
|
||||
private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> merge(
|
||||
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> a,
|
||||
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> b) {
|
||||
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
|
||||
new LinkedHashMap<>();
|
||||
combined.putAll(a);
|
||||
combined.putAll(b);
|
||||
return ImmutableMap.copyOf(combined);
|
||||
}
|
||||
|
||||
public static class DescriptionTransport {
|
||||
public final Content.Senders senders;
|
||||
public final RtpDescription description;
|
||||
public final IceUdpTransportInfo transport;
|
||||
|
||||
public DescriptionTransport(
|
||||
final Content.Senders senders,
|
||||
final RtpDescription description,
|
||||
final IceUdpTransportInfo transport) {
|
||||
this.senders = senders;
|
||||
this.description = description;
|
||||
this.transport = transport;
|
||||
public static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
|
||||
final Content content) {
|
||||
final GenericDescription description = content.getDescription();
|
||||
final GenericTransportInfo transportInfo = content.getTransport();
|
||||
final Content.Senders senders = content.getSenders();
|
||||
final RtpDescription rtpDescription;
|
||||
final IceUdpTransportInfo iceUdpTransportInfo;
|
||||
if (description == null) {
|
||||
rtpDescription = null;
|
||||
} else if (description instanceof RtpDescription) {
|
||||
rtpDescription = (RtpDescription) description;
|
||||
} else {
|
||||
throw new UnsupportedApplicationException("Content does not contain rtp description");
|
||||
}
|
||||
|
||||
public static DescriptionTransport of(final Content content) {
|
||||
final GenericDescription description = content.getDescription();
|
||||
final GenericTransportInfo transportInfo = content.getTransport();
|
||||
final Content.Senders senders = content.getSenders();
|
||||
final RtpDescription rtpDescription;
|
||||
final IceUdpTransportInfo iceUdpTransportInfo;
|
||||
if (description == null) {
|
||||
rtpDescription = null;
|
||||
} else if (description instanceof RtpDescription) {
|
||||
rtpDescription = (RtpDescription) description;
|
||||
} else {
|
||||
throw new UnsupportedApplicationException(
|
||||
"Content does not contain rtp description");
|
||||
}
|
||||
if (transportInfo instanceof IceUdpTransportInfo) {
|
||||
iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
|
||||
} else {
|
||||
throw new UnsupportedTransportException(
|
||||
"Content does not contain ICE-UDP transport");
|
||||
}
|
||||
return new DescriptionTransport(
|
||||
senders,
|
||||
rtpDescription,
|
||||
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
|
||||
}
|
||||
|
||||
private static DescriptionTransport of(
|
||||
final SessionDescription sessionDescription,
|
||||
final boolean isInitiator,
|
||||
final SessionDescription.Media media) {
|
||||
final Content.Senders senders = Content.Senders.of(media, isInitiator);
|
||||
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
|
||||
final IceUdpTransportInfo transportInfo =
|
||||
IceUdpTransportInfo.of(sessionDescription, media);
|
||||
return new DescriptionTransport(senders, rtpDescription, transportInfo);
|
||||
}
|
||||
|
||||
public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
|
||||
return ImmutableMap.copyOf(
|
||||
Maps.transformValues(
|
||||
contents, content -> content == null ? null : of(content)));
|
||||
if (transportInfo instanceof IceUdpTransportInfo) {
|
||||
iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
|
||||
} else {
|
||||
throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
|
||||
}
|
||||
return new DescriptionTransport<>(
|
||||
senders,
|
||||
rtpDescription,
|
||||
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
|
||||
}
|
||||
|
||||
public static class UnsupportedApplicationException extends IllegalArgumentException {
|
||||
UnsupportedApplicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
private static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
|
||||
final SessionDescription sessionDescription,
|
||||
final boolean isInitiator,
|
||||
final SessionDescription.Media media) {
|
||||
final Content.Senders senders = Content.Senders.of(media, isInitiator);
|
||||
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
|
||||
final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
|
||||
return new DescriptionTransport<>(senders, rtpDescription, transportInfo);
|
||||
}
|
||||
|
||||
public static class UnsupportedTransportException extends IllegalArgumentException {
|
||||
UnsupportedTransportException(String message) {
|
||||
super(message);
|
||||
}
|
||||
private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> of(
|
||||
final Map<String, Content> contents) {
|
||||
return ImmutableMap.copyOf(
|
||||
Maps.transformValues(contents, content -> content == null ? null : of(content)));
|
||||
}
|
||||
|
||||
public static final class Diff {
|
||||
|
|
|
@ -10,12 +10,17 @@ import com.google.common.base.Joiner;
|
|||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -28,6 +33,8 @@ public class SessionDescription {
|
|||
public static final String LINE_DIVIDER = "\r\n";
|
||||
private static final String HARDCODED_MEDIA_PROTOCOL =
|
||||
"UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint
|
||||
private static final String HARDCODED_APPLICATION_PROTOCOL = "UDP/DTLS/SCTP";
|
||||
private static final String FORMAT_WEBRTC_DATA_CHANNEL = "webrtc-datachannel";
|
||||
private static final int HARDCODED_MEDIA_PORT = 9;
|
||||
private static final Collection<String> HARDCODED_ICE_OPTIONS =
|
||||
Collections.singleton("trickle");
|
||||
|
@ -52,9 +59,8 @@ public class SessionDescription {
|
|||
this.media = media;
|
||||
}
|
||||
|
||||
private static void appendAttributes(
|
||||
StringBuilder s, ArrayListMultimap<String, String> attributes) {
|
||||
for (Map.Entry<String, String> attribute : attributes.entries()) {
|
||||
private static void appendAttributes(StringBuilder s, Multimap<String, String> attributes) {
|
||||
for (final Map.Entry<String, String> attribute : attributes.entries()) {
|
||||
final String key = attribute.getKey();
|
||||
final String value = attribute.getValue();
|
||||
s.append("a=").append(key);
|
||||
|
@ -79,24 +85,20 @@ public class SessionDescription {
|
|||
final char key = pair[0].charAt(0);
|
||||
final String value = pair[1];
|
||||
switch (key) {
|
||||
case 'v':
|
||||
sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
|
||||
break;
|
||||
case 'c':
|
||||
case 'v' -> sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
|
||||
case 'c' -> {
|
||||
if (currentMediaBuilder != null) {
|
||||
currentMediaBuilder.setConnectionData(value);
|
||||
} else {
|
||||
sessionDescriptionBuilder.setConnectionData(value);
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
sessionDescriptionBuilder.setName(value);
|
||||
break;
|
||||
case 'a':
|
||||
}
|
||||
case 's' -> sessionDescriptionBuilder.setName(value);
|
||||
case 'a' -> {
|
||||
final Pair<String, String> attribute = parseAttribute(value);
|
||||
attributeMap.put(attribute.first, attribute.second);
|
||||
break;
|
||||
case 'm':
|
||||
}
|
||||
case 'm' -> {
|
||||
if (currentMediaBuilder == null) {
|
||||
sessionDescriptionBuilder.setAttributes(attributeMap);
|
||||
} else {
|
||||
|
@ -118,7 +120,7 @@ public class SessionDescription {
|
|||
} else {
|
||||
Log.d(Config.LOGTAG, "skipping media line " + line);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentMediaBuilder != null) {
|
||||
|
@ -131,6 +133,56 @@ public class SessionDescription {
|
|||
return sessionDescriptionBuilder.createSessionDescription();
|
||||
}
|
||||
|
||||
public static SessionDescription of(final FileTransferContentMap contentMap) {
|
||||
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
|
||||
final ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
|
||||
final ImmutableList.Builder<Media> mediaListBuilder = new ImmutableList.Builder<>();
|
||||
|
||||
final Group group = contentMap.group;
|
||||
if (group != null) {
|
||||
final String semantics = group.getSemantics();
|
||||
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
|
||||
final var idTags = group.getIdentificationTags();
|
||||
for (final String content : idTags) {
|
||||
checkNoWhitespace(content, "group content names must not contain any whitespace");
|
||||
}
|
||||
attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags));
|
||||
}
|
||||
|
||||
// TODO my-media-stream can be removed I think
|
||||
attributeMap.put("msid-semantic", " WMS my-media-stream");
|
||||
|
||||
for (final Map.Entry<
|
||||
String, DescriptionTransport<FileTransferDescription, GenericTransportInfo>>
|
||||
entry : contentMap.contents.entrySet()) {
|
||||
final var dt = entry.getValue();
|
||||
final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo;
|
||||
if (dt.transport instanceof WebRTCDataChannelTransportInfo transportInfo) {
|
||||
webRTCDataChannelTransportInfo = transportInfo;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Transport is not of type WebRTCDataChannel");
|
||||
}
|
||||
final String name = entry.getKey();
|
||||
checkNoWhitespace(name, "content name must not contain any whitespace");
|
||||
|
||||
final MediaBuilder mediaBuilder = new MediaBuilder();
|
||||
mediaBuilder.setMedia("application");
|
||||
mediaBuilder.setConnectionData(HARDCODED_CONNECTION);
|
||||
mediaBuilder.setPort(HARDCODED_MEDIA_PORT);
|
||||
mediaBuilder.setProtocol(HARDCODED_APPLICATION_PROTOCOL);
|
||||
mediaBuilder.setAttributes(
|
||||
transportInfoMediaAttributes(webRTCDataChannelTransportInfo));
|
||||
mediaBuilder.setFormat(FORMAT_WEBRTC_DATA_CHANNEL);
|
||||
mediaListBuilder.add(mediaBuilder.createMedia());
|
||||
}
|
||||
|
||||
sessionDescriptionBuilder.setVersion(0);
|
||||
sessionDescriptionBuilder.setName("-");
|
||||
sessionDescriptionBuilder.setMedia(mediaListBuilder.build());
|
||||
sessionDescriptionBuilder.setAttributes(attributeMap);
|
||||
return sessionDescriptionBuilder.createSessionDescription();
|
||||
}
|
||||
|
||||
public static SessionDescription of(
|
||||
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
|
||||
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
|
||||
|
@ -140,58 +192,27 @@ public class SessionDescription {
|
|||
if (group != null) {
|
||||
final String semantics = group.getSemantics();
|
||||
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
|
||||
attributeMap.put(
|
||||
"group",
|
||||
group.getSemantics()
|
||||
+ " "
|
||||
+ Joiner.on(' ').join(group.getIdentificationTags()));
|
||||
final var idTags = group.getIdentificationTags();
|
||||
for (final String content : idTags) {
|
||||
checkNoWhitespace(content, "group content names must not contain any whitespace");
|
||||
}
|
||||
attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags));
|
||||
}
|
||||
|
||||
// TODO my-media-stream can be removed I think
|
||||
attributeMap.put("msid-semantic", " WMS my-media-stream");
|
||||
|
||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry :
|
||||
contentMap.contents.entrySet()) {
|
||||
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||
entry : contentMap.contents.entrySet()) {
|
||||
final String name = entry.getKey();
|
||||
RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
|
||||
RtpDescription description = descriptionTransport.description;
|
||||
IceUdpTransportInfo transport = descriptionTransport.transport;
|
||||
checkNoWhitespace(name, "content name must not contain any whitespace");
|
||||
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
|
||||
entry.getValue();
|
||||
final RtpDescription description = descriptionTransport.description;
|
||||
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
||||
final String ufrag = transport.getAttribute("ufrag");
|
||||
final String pwd = transport.getAttribute("pwd");
|
||||
if (Strings.isNullOrEmpty(ufrag)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing required ufrag attribute");
|
||||
}
|
||||
checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
|
||||
mediaAttributes.put("ice-ufrag", ufrag);
|
||||
if (Strings.isNullOrEmpty(pwd)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing required pwd attribute");
|
||||
}
|
||||
checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
|
||||
mediaAttributes.put("ice-pwd", pwd);
|
||||
final List<String> negotiatedIceOptions = transport.getIceOptions();
|
||||
final Collection<String> iceOptions =
|
||||
negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions;
|
||||
mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions));
|
||||
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
||||
if (fingerprint != null) {
|
||||
final String hashFunction = fingerprint.getHash();
|
||||
final String hash = fingerprint.getContent();
|
||||
if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) {
|
||||
throw new IllegalArgumentException("DTLS-SRTP missing hash");
|
||||
}
|
||||
checkNoWhitespace(
|
||||
hashFunction, "DTLS-SRTP hash function must not contain whitespace");
|
||||
checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace");
|
||||
mediaAttributes.put("fingerprint", hashFunction + " " + hash);
|
||||
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
|
||||
if (setup != null) {
|
||||
mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
mediaAttributes.putAll(transportInfoMediaAttributes(descriptionTransport.transport));
|
||||
final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>();
|
||||
for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
|
||||
for (final RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
|
||||
final String id = payloadType.getId();
|
||||
if (Strings.isNullOrEmpty(id)) {
|
||||
throw new IllegalArgumentException("Payload type is missing id");
|
||||
|
@ -353,6 +374,69 @@ public class SessionDescription {
|
|||
return sessionDescriptionBuilder.createSessionDescription();
|
||||
}
|
||||
|
||||
private static Multimap<String, String> transportInfoMediaAttributes(
|
||||
final IceUdpTransportInfo transport) {
|
||||
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
||||
final String ufrag = transport.getAttribute("ufrag");
|
||||
final String pwd = transport.getAttribute("pwd");
|
||||
if (Strings.isNullOrEmpty(ufrag)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing required ufrag attribute");
|
||||
}
|
||||
checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
|
||||
mediaAttributes.put("ice-ufrag", ufrag);
|
||||
if (Strings.isNullOrEmpty(pwd)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing required pwd attribute");
|
||||
}
|
||||
checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
|
||||
mediaAttributes.put("ice-pwd", pwd);
|
||||
final List<String> negotiatedIceOptions = transport.getIceOptions();
|
||||
final Collection<String> iceOptions =
|
||||
negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions;
|
||||
mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions));
|
||||
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
||||
if (fingerprint != null) {
|
||||
final String hashFunction = fingerprint.getHash();
|
||||
final String hash = fingerprint.getContent();
|
||||
if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) {
|
||||
throw new IllegalArgumentException("DTLS-SRTP missing hash");
|
||||
}
|
||||
checkNoWhitespace(hashFunction, "DTLS-SRTP hash function must not contain whitespace");
|
||||
checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace");
|
||||
mediaAttributes.put("fingerprint", hashFunction + " " + hash);
|
||||
final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
|
||||
if (setup != null) {
|
||||
mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return ImmutableMultimap.copyOf(mediaAttributes);
|
||||
}
|
||||
|
||||
private static Multimap<String, String> transportInfoMediaAttributes(
|
||||
final WebRTCDataChannelTransportInfo transport) {
|
||||
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
||||
final var iceUdpTransportInfo = transport.innerIceUdpTransportInfo();
|
||||
if (iceUdpTransportInfo == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing inner ice-udp transport");
|
||||
}
|
||||
mediaAttributes.putAll(transportInfoMediaAttributes(iceUdpTransportInfo));
|
||||
final Integer sctpPort = transport.getSctpPort();
|
||||
if (sctpPort == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing required sctp-port attribute");
|
||||
}
|
||||
mediaAttributes.put("sctp-port", String.valueOf(sctpPort));
|
||||
final Integer maxMessageSize = transport.getMaxMessageSize();
|
||||
if (maxMessageSize == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Transport element is missing required max-message-size");
|
||||
}
|
||||
mediaAttributes.put("max-message-size", String.valueOf(maxMessageSize));
|
||||
return ImmutableMultimap.copyOf(mediaAttributes);
|
||||
}
|
||||
|
||||
public static String checkNoWhitespace(final String input, final String message) {
|
||||
if (CharMatcher.whitespace().matchesAnyOf(input)) {
|
||||
throw new IllegalArgumentException(message);
|
||||
|
@ -421,7 +505,7 @@ public class SessionDescription {
|
|||
.append(' ')
|
||||
.append(media.protocol)
|
||||
.append(' ')
|
||||
.append(Joiner.on(' ').join(media.formats))
|
||||
.append(media.format)
|
||||
.append(LINE_DIVIDER);
|
||||
s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
|
||||
appendAttributes(s, media.attributes);
|
||||
|
@ -433,21 +517,21 @@ public class SessionDescription {
|
|||
public final String media;
|
||||
public final int port;
|
||||
public final String protocol;
|
||||
public final List<Integer> formats;
|
||||
public final String format;
|
||||
public final String connectionData;
|
||||
public final ArrayListMultimap<String, String> attributes;
|
||||
public final Multimap<String, String> attributes;
|
||||
|
||||
public Media(
|
||||
String media,
|
||||
int port,
|
||||
String protocol,
|
||||
List<Integer> formats,
|
||||
String format,
|
||||
String connectionData,
|
||||
ArrayListMultimap<String, String> attributes) {
|
||||
Multimap<String, String> attributes) {
|
||||
this.media = media;
|
||||
this.port = port;
|
||||
this.protocol = protocol;
|
||||
this.formats = formats;
|
||||
this.format = format;
|
||||
this.connectionData = connectionData;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
|
|
@ -406,7 +406,7 @@ public class WebRTCWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
private static PeerConnection.RTCConfiguration buildConfiguration(
|
||||
public static PeerConnection.RTCConfiguration buildConfiguration(
|
||||
final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
|
||||
final PeerConnection.RTCConfiguration rtcConfig =
|
||||
new PeerConnection.RTCConfiguration(iceServers);
|
||||
|
@ -774,7 +774,7 @@ public class WebRTCWrapper {
|
|||
void onRenegotiationNeeded();
|
||||
}
|
||||
|
||||
private abstract static class SetSdpObserver implements SdpObserver {
|
||||
public abstract static class SetSdpObserver implements SdpObserver {
|
||||
|
||||
@Override
|
||||
public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
|
||||
|
@ -800,12 +800,12 @@ public class WebRTCWrapper {
|
|||
|
||||
public static class PeerConnectionNotInitialized extends IllegalStateException {
|
||||
|
||||
private PeerConnectionNotInitialized() {
|
||||
public PeerConnectionNotInitialized() {
|
||||
super("initialize PeerConnection first");
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailureToSetDescriptionException extends IllegalArgumentException {
|
||||
public static class FailureToSetDescriptionException extends IllegalArgumentException {
|
||||
public FailureToSetDescriptionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
|
|
@ -8,14 +8,14 @@ import com.google.common.base.Preconditions;
|
|||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class Content extends Element {
|
||||
|
||||
public Content(final Creator creator, final Senders senders, final String name) {
|
||||
|
@ -65,7 +65,7 @@ public class Content extends Element {
|
|||
return null;
|
||||
}
|
||||
final String namespace = description.getNamespace();
|
||||
if (FileTransferDescription.NAMESPACES.contains(namespace)) {
|
||||
if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
|
||||
return FileTransferDescription.upgrade(description);
|
||||
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
|
||||
return RtpDescription.upgrade(description);
|
||||
|
@ -90,9 +90,11 @@ public class Content extends Element {
|
|||
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
|
||||
return IbbTransportInfo.upgrade(transport);
|
||||
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
|
||||
return S5BTransportInfo.upgrade(transport);
|
||||
return SocksByteStreamsTransportInfo.upgrade(transport);
|
||||
} else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
|
||||
return IceUdpTransportInfo.upgrade(transport);
|
||||
} else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) {
|
||||
return WebRTCDataChannelTransportInfo.upgrade(transport);
|
||||
} else if (transport != null) {
|
||||
return GenericTransportInfo.upgrade(transport);
|
||||
} else {
|
||||
|
@ -100,7 +102,6 @@ public class Content extends Element {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public void setTransport(GenericTransportInfo transportInfo) {
|
||||
this.addChild(transportInfo);
|
||||
}
|
||||
|
@ -141,7 +142,7 @@ public class Content extends Element {
|
|||
} else if (attributes.contains("recvonly")) {
|
||||
return initiator ? RESPONDER : INITIATOR;
|
||||
}
|
||||
Log.w(Config.LOGTAG,"assuming default value for senders");
|
||||
Log.w(Config.LOGTAG, "assuming default value for senders");
|
||||
// If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
|
||||
// present, "sendrecv" SHOULD be assumed as the default
|
||||
// https://www.rfc-editor.org/rfc/rfc4566
|
||||
|
|
|
@ -1,89 +1,233 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FileTransferDescription extends GenericDescription {
|
||||
|
||||
public static List<String> NAMESPACES = Arrays.asList(
|
||||
Version.FT_3.namespace,
|
||||
Version.FT_4.namespace,
|
||||
Version.FT_5.namespace
|
||||
);
|
||||
|
||||
|
||||
private FileTransferDescription(String name, String namespace) {
|
||||
super(name, namespace);
|
||||
private FileTransferDescription() {
|
||||
super("description", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
final String namespace = getNamespace();
|
||||
if (namespace.equals(Version.FT_3.namespace)) {
|
||||
return Version.FT_3;
|
||||
} else if (namespace.equals(Version.FT_4.namespace)) {
|
||||
return Version.FT_4;
|
||||
} else if (namespace.equals(Version.FT_5.namespace)) {
|
||||
return Version.FT_5;
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown namespace");
|
||||
}
|
||||
}
|
||||
|
||||
public Element getFileOffer() {
|
||||
final Version version = getVersion();
|
||||
if (version == Version.FT_3) {
|
||||
final Element offer = this.findChild("offer");
|
||||
return offer == null ? null : offer.findChild("file");
|
||||
} else {
|
||||
return this.findChild("file");
|
||||
}
|
||||
}
|
||||
|
||||
public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) {
|
||||
final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace());
|
||||
final Element fileElement;
|
||||
if (version == Version.FT_3) {
|
||||
Element offer = description.addChild("offer");
|
||||
fileElement = offer.addChild("file");
|
||||
} else {
|
||||
fileElement = description.addChild("file");
|
||||
}
|
||||
fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
|
||||
fileElement.addChild("name").setContent(file.getName());
|
||||
if (axolotlMessage != null) {
|
||||
fileElement.addChild(axolotlMessage.toElement());
|
||||
public static FileTransferDescription of(final File fileDescription) {
|
||||
final var description = new FileTransferDescription();
|
||||
final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
file.addChild("name").setContent(fileDescription.name);
|
||||
file.addChild("size").setContent(Long.toString(fileDescription.size));
|
||||
if (fileDescription.mediaType != null) {
|
||||
file.addChild("mediaType").setContent(fileDescription.mediaType);
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
final Element fileElement = this.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
if (fileElement == null) {
|
||||
Log.d(Config.LOGTAG,"no file? "+this);
|
||||
throw new IllegalStateException("file transfer description has no file");
|
||||
}
|
||||
final String name = fileElement.findChildContent("name");
|
||||
final String sizeAsString = fileElement.findChildContent("size");
|
||||
final String mediaType = fileElement.findChildContent("mediaType");
|
||||
if (Strings.isNullOrEmpty(name) || Strings.isNullOrEmpty(sizeAsString)) {
|
||||
throw new IllegalStateException("File definition is missing name and/or size");
|
||||
}
|
||||
final Long size = Longs.tryParse(sizeAsString);
|
||||
if (size == null) {
|
||||
throw new IllegalStateException("Invalid file size");
|
||||
}
|
||||
final List<Hash> hashes = findHashes(fileElement.getChildren());
|
||||
return new File(size, name, mediaType, hashes);
|
||||
}
|
||||
|
||||
public static SessionInfo getSessionInfo(@NonNull final JinglePacket jinglePacket) {
|
||||
Preconditions.checkNotNull(jinglePacket);
|
||||
Preconditions.checkArgument(
|
||||
jinglePacket.getAction() == JinglePacket.Action.SESSION_INFO,
|
||||
"jingle packet is not a session-info");
|
||||
final Element jingle = jinglePacket.findChild("jingle", Namespace.JINGLE);
|
||||
if (jingle == null) {
|
||||
return null;
|
||||
}
|
||||
final Element checksum = jingle.findChild("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
if (checksum != null) {
|
||||
final Element file = checksum.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
final String name = checksum.getAttribute("name");
|
||||
if (file == null || Strings.isNullOrEmpty(name)) {
|
||||
return null;
|
||||
}
|
||||
return new Checksum(name, findHashes(file.getChildren()));
|
||||
}
|
||||
final Element received = jingle.findChild("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
if (received != null) {
|
||||
final String name = received.getAttribute("name");
|
||||
if (Strings.isNullOrEmpty(name)) {
|
||||
return new Received(name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<Hash> findHashes(final List<Element> elements) {
|
||||
final ImmutableList.Builder<Hash> hashes = new ImmutableList.Builder<>();
|
||||
for (final Element child : elements) {
|
||||
if ("hash".equals(child.getName()) && Namespace.HASHES.equals(child.getNamespace())) {
|
||||
final Algorithm algorithm;
|
||||
try {
|
||||
algorithm = Algorithm.of(child.getAttribute("algo"));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
final String content = child.getContent();
|
||||
if (Strings.isNullOrEmpty(content)) {
|
||||
continue;
|
||||
}
|
||||
if (BaseEncoding.base64().canDecode(content)) {
|
||||
hashes.add(new Hash(BaseEncoding.base64().decode(content), algorithm));
|
||||
}
|
||||
}
|
||||
}
|
||||
return hashes.build();
|
||||
}
|
||||
|
||||
public static FileTransferDescription upgrade(final Element element) {
|
||||
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
|
||||
Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace");
|
||||
final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace());
|
||||
Preconditions.checkArgument(
|
||||
"description".equals(element.getName()),
|
||||
"Name of provided element is not description");
|
||||
Preconditions.checkArgument(
|
||||
element.getNamespace().equals(Namespace.JINGLE_APPS_FILE_TRANSFER),
|
||||
"Element does not match a file transfer namespace");
|
||||
final FileTransferDescription description = new FileTransferDescription();
|
||||
description.setAttributes(element.getAttributes());
|
||||
description.setChildren(element.getChildren());
|
||||
return description;
|
||||
}
|
||||
|
||||
public enum Version {
|
||||
FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
|
||||
FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
|
||||
FT_5("urn:xmpp:jingle:apps:file-transfer:5");
|
||||
public static final class Checksum extends SessionInfo {
|
||||
public final List<Hash> hashes;
|
||||
|
||||
private final String namespace;
|
||||
|
||||
Version(String namespace) {
|
||||
this.namespace = namespace;
|
||||
public Checksum(final String name, List<Hash> hashes) {
|
||||
super(name);
|
||||
this.hashes = hashes;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return namespace;
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this).add("hashes", hashes).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element asElement() {
|
||||
final var checksum = new Element("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
checksum.setAttribute("name", name);
|
||||
final var file = checksum.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
for (final Hash hash : hashes) {
|
||||
final var element = file.addChild("hash", Namespace.HASHES);
|
||||
element.setAttribute(
|
||||
"algo",
|
||||
CaseFormat.UPPER_UNDERSCORE.to(
|
||||
CaseFormat.LOWER_HYPHEN, hash.algorithm.toString()));
|
||||
element.setContent(BaseEncoding.base64().encode(hash.hash));
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Received extends SessionInfo {
|
||||
|
||||
public Received(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element asElement() {
|
||||
final var element = new Element("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||
element.setAttribute("name", name);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static sealed class SessionInfo permits Checksum, Received {
|
||||
|
||||
public final String name;
|
||||
|
||||
protected SessionInfo(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public abstract Element asElement();
|
||||
}
|
||||
|
||||
public static class File {
|
||||
public final long size;
|
||||
public final String name;
|
||||
public final String mediaType;
|
||||
|
||||
public final List<Hash> hashes;
|
||||
|
||||
public File(long size, String name, String mediaType, List<Hash> hashes) {
|
||||
this.size = size;
|
||||
this.name = name;
|
||||
this.mediaType = mediaType;
|
||||
this.hashes = hashes;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("size", size)
|
||||
.add("name", name)
|
||||
.add("mediaType", mediaType)
|
||||
.add("hashes", hashes)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Hash {
|
||||
public final byte[] hash;
|
||||
public final Algorithm algorithm;
|
||||
|
||||
public Hash(byte[] hash, Algorithm algorithm) {
|
||||
this.hash = hash;
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("hash", hash)
|
||||
.add("algorithm", algorithm)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public enum Algorithm {
|
||||
SHA_1,
|
||||
SHA_256;
|
||||
|
||||
public static Algorithm of(final String value) {
|
||||
if (Strings.isNullOrEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
return valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ public class GenericDescription extends Element {
|
|||
|
||||
GenericDescription(String name, final String namespace) {
|
||||
super(name, namespace);
|
||||
Preconditions.checkArgument("description".equals(name));
|
||||
}
|
||||
|
||||
public static GenericDescription upgrade(final Element element) {
|
||||
|
|
|
@ -41,7 +41,7 @@ public class Group extends Element {
|
|||
}
|
||||
|
||||
public static Group ofSdpString(final String input) {
|
||||
ImmutableList.Builder<String> tagBuilder = new ImmutableList.Builder<>();
|
||||
final ImmutableList.Builder<String> tagBuilder = new ImmutableList.Builder<>();
|
||||
final String[] parts = input.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
final String semantics = parts[0];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
|
@ -23,16 +25,9 @@ public class IbbTransportInfo extends GenericTransportInfo {
|
|||
return this.getAttribute("sid");
|
||||
}
|
||||
|
||||
public int getBlockSize() {
|
||||
public Long getBlockSize() {
|
||||
final String blockSize = this.getAttribute("block-size");
|
||||
if (blockSize == null) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(blockSize);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
return Strings.isNullOrEmpty(blockSize) ? null : Longs.tryParse(blockSize);
|
||||
}
|
||||
|
||||
public static IbbTransportInfo upgrade(final Element element) {
|
||||
|
|
|
@ -15,11 +15,13 @@ import com.google.common.collect.Collections2;
|
|||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
@ -195,7 +197,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
|||
}
|
||||
}
|
||||
|
||||
public static class Candidate extends Element {
|
||||
public static class Candidate extends Element implements Transport.Candidate {
|
||||
|
||||
private Candidate() {
|
||||
super("candidate");
|
||||
|
@ -396,7 +398,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
|||
return fingerprint;
|
||||
}
|
||||
|
||||
private static Fingerprint of(ArrayListMultimap<String, String> attributes) {
|
||||
private static Fingerprint of(final Multimap<String, String> attributes) {
|
||||
final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
|
||||
final String setup = Iterables.getFirst(attributes.get("setup"), null);
|
||||
if (setup != null && fingerprint != null) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
|
@ -7,13 +9,16 @@ import com.google.common.base.Preconditions;
|
|||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class JinglePacket extends IqPacket {
|
||||
|
||||
private JinglePacket() {
|
||||
|
@ -36,7 +41,7 @@ public class JinglePacket extends IqPacket {
|
|||
return jinglePacket;
|
||||
}
|
||||
|
||||
//TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
|
||||
// TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
|
||||
public Content getJingleContent() {
|
||||
final Element content = getJingleChild("content");
|
||||
return content == null ? null : Content.upgrade(content);
|
||||
|
@ -64,7 +69,7 @@ public class JinglePacket extends IqPacket {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
public void addJingleContent(final Content content) { //take content interface
|
||||
public void addJingleContent(final Content content) { // take content interface
|
||||
addJingleChild(content);
|
||||
}
|
||||
|
||||
|
@ -94,13 +99,13 @@ public class JinglePacket extends IqPacket {
|
|||
}
|
||||
}
|
||||
|
||||
//RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
|
||||
// RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
|
||||
public void setInitiator(final Jid initiator) {
|
||||
Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID");
|
||||
findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator);
|
||||
}
|
||||
|
||||
//RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
|
||||
// RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
|
||||
public void setResponder(Jid responder) {
|
||||
Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID");
|
||||
findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder);
|
||||
|
@ -116,6 +121,39 @@ public class JinglePacket extends IqPacket {
|
|||
jingle.addChild(child);
|
||||
}
|
||||
|
||||
public void setSecurity(final String name, final XmppAxolotlMessage xmppAxolotlMessage) {
|
||||
final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
|
||||
security.setAttribute("name", name);
|
||||
security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding");
|
||||
security.setAttribute("type", AxolotlService.PEP_PREFIX);
|
||||
security.addChild(xmppAxolotlMessage.toElement());
|
||||
addJingleChild(security);
|
||||
}
|
||||
|
||||
public XmppAxolotlMessage getSecurity(final String nameNeedle) {
|
||||
final Element jingle = findChild("jingle", Namespace.JINGLE);
|
||||
if (jingle == null) {
|
||||
return null;
|
||||
}
|
||||
for (final Element child : jingle.getChildren()) {
|
||||
if ("security".equals(child.getName())
|
||||
&& Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) {
|
||||
final String name = child.getAttribute("name");
|
||||
final String type = child.getAttribute("type");
|
||||
final String cipher = child.getAttribute("cipher");
|
||||
if (nameNeedle.equals(name)
|
||||
&& AxolotlService.PEP_PREFIX.equals(type)
|
||||
&& "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) {
|
||||
final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX);
|
||||
if (encrypted != null) {
|
||||
return XmppAxolotlMessage.fromElement(encrypted, getFrom().asBareJid());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
|
||||
}
|
||||
|
@ -142,7 +180,7 @@ public class JinglePacket extends IqPacket {
|
|||
TRANSPORT_REPLACE;
|
||||
|
||||
public static Action of(final String value) {
|
||||
//TODO handle invalid
|
||||
// TODO handle invalid
|
||||
return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
|
||||
}
|
||||
|
||||
|
@ -153,7 +191,6 @@ public class JinglePacket extends IqPacket {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static class ReasonWrapper {
|
||||
public final Reason reason;
|
||||
public final String text;
|
||||
|
|
|
@ -18,7 +18,7 @@ public class Propose extends Element {
|
|||
for (final Element child : this.children) {
|
||||
if ("description".equals(child.getName())) {
|
||||
final String namespace = child.getNamespace();
|
||||
if (FileTransferDescription.NAMESPACES.contains(namespace)) {
|
||||
if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
|
||||
builder.add(FileTransferDescription.upgrade(child));
|
||||
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
|
||||
builder.add(RtpDescription.upgrade(child));
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.JingleCandidate;
|
||||
|
||||
public class S5BTransportInfo extends GenericTransportInfo {
|
||||
|
||||
private S5BTransportInfo(final String name, final String xmlns) {
|
||||
super(name, xmlns);
|
||||
}
|
||||
|
||||
public String getTransportId() {
|
||||
return this.getAttribute("sid");
|
||||
}
|
||||
|
||||
public S5BTransportInfo(final String transportId, final Collection<JingleCandidate> candidates) {
|
||||
super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
Preconditions.checkNotNull(transportId,"transport id must not be null");
|
||||
for(JingleCandidate candidate : candidates) {
|
||||
this.addChild(candidate.toElement());
|
||||
}
|
||||
this.setAttribute("sid", transportId);
|
||||
}
|
||||
|
||||
public S5BTransportInfo(final String transportId, final Element child) {
|
||||
super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
Preconditions.checkNotNull(transportId,"transport id must not be null");
|
||||
this.addChild(child);
|
||||
this.setAttribute("sid", transportId);
|
||||
}
|
||||
|
||||
public List<JingleCandidate> getCandidates() {
|
||||
return JingleCandidate.parse(this.getChildren());
|
||||
}
|
||||
|
||||
public static S5BTransportInfo upgrade(final Element element) {
|
||||
Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport");
|
||||
Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()), "Element does not match s5b transport namespace");
|
||||
final S5BTransportInfo transportInfo = new S5BTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
transportInfo.setAttributes(element.getAttributes());
|
||||
transportInfo.setChildren(element.getChildren());
|
||||
return transportInfo;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class SocksByteStreamsTransportInfo extends GenericTransportInfo {
|
||||
|
||||
private SocksByteStreamsTransportInfo() {
|
||||
super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
}
|
||||
|
||||
public String getTransportId() {
|
||||
return this.getAttribute("sid");
|
||||
}
|
||||
|
||||
public SocksByteStreamsTransportInfo(
|
||||
final String transportId,
|
||||
final Collection<SocksByteStreamsTransport.Candidate> candidates) {
|
||||
super("transport", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
Preconditions.checkNotNull(transportId, "transport id must not be null");
|
||||
for (SocksByteStreamsTransport.Candidate candidate : candidates) {
|
||||
this.addChild(candidate.asElement());
|
||||
}
|
||||
this.setAttribute("sid", transportId);
|
||||
}
|
||||
|
||||
public TransportInfo getTransportInfo() {
|
||||
if (hasChild("proxy-error")) {
|
||||
return new ProxyError();
|
||||
} else if (hasChild("candidate-error")) {
|
||||
return new CandidateError();
|
||||
} else if (hasChild("candidate-used")) {
|
||||
final Element candidateUsed = findChild("candidate-used");
|
||||
final String cid = candidateUsed == null ? null : candidateUsed.getAttribute("cid");
|
||||
if (Strings.isNullOrEmpty(cid)) {
|
||||
return null;
|
||||
} else {
|
||||
return new CandidateUsed(cid);
|
||||
}
|
||||
} else if (hasChild("activated")) {
|
||||
final Element activated = findChild("activated");
|
||||
final String cid = activated == null ? null : activated.getAttribute("cid");
|
||||
if (Strings.isNullOrEmpty(cid)) {
|
||||
return null;
|
||||
} else {
|
||||
return new Activated(cid);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<SocksByteStreamsTransport.Candidate> getCandidates() {
|
||||
final ImmutableList.Builder<SocksByteStreamsTransport.Candidate> candidateBuilder =
|
||||
new ImmutableList.Builder<>();
|
||||
for (final Element child : this.children) {
|
||||
if ("candidate".equals(child.getName())
|
||||
&& Namespace.JINGLE_TRANSPORTS_S5B.equals(child.getNamespace())) {
|
||||
try {
|
||||
candidateBuilder.add(SocksByteStreamsTransport.Candidate.of(child));
|
||||
} catch (final Exception e) {
|
||||
Log.d(Config.LOGTAG, "skip over broken candidate", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidateBuilder.build();
|
||||
}
|
||||
|
||||
public static SocksByteStreamsTransportInfo upgrade(final Element element) {
|
||||
Preconditions.checkArgument(
|
||||
"transport".equals(element.getName()), "Name of provided element is not transport");
|
||||
Preconditions.checkArgument(
|
||||
Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()),
|
||||
"Element does not match s5b transport namespace");
|
||||
final SocksByteStreamsTransportInfo transportInfo = new SocksByteStreamsTransportInfo();
|
||||
transportInfo.setAttributes(element.getAttributes());
|
||||
transportInfo.setChildren(element.getChildren());
|
||||
return transportInfo;
|
||||
}
|
||||
|
||||
public String getDestinationAddress() {
|
||||
return this.getAttribute("dstaddr");
|
||||
}
|
||||
|
||||
public abstract static class TransportInfo {}
|
||||
|
||||
public static class CandidateUsed extends TransportInfo {
|
||||
public final String cid;
|
||||
|
||||
public CandidateUsed(String cid) {
|
||||
this.cid = cid;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Activated extends TransportInfo {
|
||||
public final String cid;
|
||||
|
||||
public Activated(final String cid) {
|
||||
this.cid = cid;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CandidateError extends TransportInfo {}
|
||||
|
||||
public static class ProxyError extends TransportInfo {}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.primitives.Ints;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
|
||||
|
||||
public class WebRTCDataChannelTransportInfo extends GenericTransportInfo {
|
||||
|
||||
public static final WebRTCDataChannelTransportInfo STUB = new WebRTCDataChannelTransportInfo();
|
||||
|
||||
public WebRTCDataChannelTransportInfo() {
|
||||
super("transport", Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
|
||||
}
|
||||
|
||||
public static WebRTCDataChannelTransportInfo upgrade(final Element element) {
|
||||
Preconditions.checkArgument(
|
||||
"transport".equals(element.getName()), "Name of provided element is not transport");
|
||||
Preconditions.checkArgument(
|
||||
Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(element.getNamespace()),
|
||||
"Element does not match ice-udp transport namespace");
|
||||
final WebRTCDataChannelTransportInfo transportInfo = new WebRTCDataChannelTransportInfo();
|
||||
transportInfo.setAttributes(element.getAttributes());
|
||||
transportInfo.setChildren(element.getChildren());
|
||||
return transportInfo;
|
||||
}
|
||||
|
||||
public IceUdpTransportInfo innerIceUdpTransportInfo() {
|
||||
final var iceUdpTransportInfo =
|
||||
this.findChild("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP);
|
||||
if (iceUdpTransportInfo != null) {
|
||||
return IceUdpTransportInfo.upgrade(iceUdpTransportInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Transport.InitialTransportInfo of(final SessionDescription sessionDescription) {
|
||||
final SessionDescription.Media media = Iterables.getOnlyElement(sessionDescription.media);
|
||||
final String id = Iterables.getFirst(media.attributes.get("mid"), null);
|
||||
Preconditions.checkNotNull(id, "media has no mid");
|
||||
final String maxMessageSize =
|
||||
Iterables.getFirst(media.attributes.get("max-message-size"), null);
|
||||
final Integer maxMessageSizeInt =
|
||||
maxMessageSize == null ? null : Ints.tryParse(maxMessageSize);
|
||||
final String sctpPort = Iterables.getFirst(media.attributes.get("sctp-port"), null);
|
||||
final Integer sctpPortInt = sctpPort == null ? null : Ints.tryParse(sctpPort);
|
||||
final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo =
|
||||
new WebRTCDataChannelTransportInfo();
|
||||
if (maxMessageSizeInt != null) {
|
||||
webRTCDataChannelTransportInfo.setAttribute("max-message-size", maxMessageSizeInt);
|
||||
}
|
||||
if (sctpPortInt != null) {
|
||||
webRTCDataChannelTransportInfo.setAttribute("sctp-port", sctpPortInt);
|
||||
}
|
||||
webRTCDataChannelTransportInfo.addChild(IceUdpTransportInfo.of(sessionDescription, media));
|
||||
|
||||
final String groupAttribute =
|
||||
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
|
||||
final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute);
|
||||
return new Transport.InitialTransportInfo(id, webRTCDataChannelTransportInfo, group);
|
||||
}
|
||||
|
||||
public Integer getSctpPort() {
|
||||
final var attribute = this.getAttribute("sctp-port");
|
||||
if (attribute == null) {
|
||||
return null;
|
||||
}
|
||||
return Ints.tryParse(attribute);
|
||||
}
|
||||
|
||||
public Integer getMaxMessageSize() {
|
||||
final var attribute = this.getAttribute("max-message-size");
|
||||
if (attribute == null) {
|
||||
return null;
|
||||
}
|
||||
return Ints.tryParse(attribute);
|
||||
}
|
||||
|
||||
public WebRTCDataChannelTransportInfo cloneWrapper() {
|
||||
final var iceUdpTransport = this.innerIceUdpTransportInfo();
|
||||
final WebRTCDataChannelTransportInfo transportInfo = new WebRTCDataChannelTransportInfo();
|
||||
transportInfo.setAttributes(new Hashtable<>(getAttributes()));
|
||||
transportInfo.addChild(iceUdpTransport.cloneWrapper());
|
||||
return transportInfo;
|
||||
}
|
||||
|
||||
public void addCandidate(final IceUdpTransportInfo.Candidate candidate) {
|
||||
this.innerIceUdpTransportInfo().addChild(candidate);
|
||||
}
|
||||
|
||||
public List<IceUdpTransportInfo.Candidate> getCandidates() {
|
||||
final var innerTransportInfo = this.innerIceUdpTransportInfo();
|
||||
if (innerTransportInfo == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return innerTransportInfo.getCandidates();
|
||||
}
|
||||
|
||||
public IceUdpTransportInfo.Credentials getCredentials() {
|
||||
final var innerTransportInfo = this.innerIceUdpTransportInfo();
|
||||
return innerTransportInfo == null ? null : innerTransportInfo.getCredentials();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,321 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.transports;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.common.io.Closeables;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class InbandBytestreamsTransport implements Transport {
|
||||
|
||||
private static final int DEFAULT_BLOCK_SIZE = 8192;
|
||||
|
||||
private final PipedInputStream pipedInputStream = new PipedInputStream(DEFAULT_BLOCK_SIZE);
|
||||
private final PipedOutputStream pipedOutputStream = new PipedOutputStream();
|
||||
private final CountDownLatch terminationLatch = new CountDownLatch(1);
|
||||
|
||||
private final XmppConnection xmppConnection;
|
||||
|
||||
private final Jid with;
|
||||
|
||||
private final boolean initiator;
|
||||
|
||||
private final String streamId;
|
||||
|
||||
private int blockSize;
|
||||
private Callback transportCallback;
|
||||
private final BlockSender blockSender;
|
||||
|
||||
private final Thread blockSenderThread;
|
||||
|
||||
private final AtomicBoolean isReceiving = new AtomicBoolean(false);
|
||||
|
||||
public InbandBytestreamsTransport(
|
||||
final XmppConnection xmppConnection, final Jid with, final boolean initiator) {
|
||||
this(xmppConnection, with, initiator, UUID.randomUUID().toString(), DEFAULT_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
public InbandBytestreamsTransport(
|
||||
final XmppConnection xmppConnection,
|
||||
final Jid with,
|
||||
final boolean initiator,
|
||||
final String streamId,
|
||||
final int blockSize) {
|
||||
this.xmppConnection = xmppConnection;
|
||||
this.with = with;
|
||||
this.initiator = initiator;
|
||||
this.streamId = streamId;
|
||||
this.blockSize = Math.min(DEFAULT_BLOCK_SIZE, blockSize);
|
||||
this.blockSender =
|
||||
new BlockSender(xmppConnection, with, streamId, this.blockSize, pipedInputStream);
|
||||
this.blockSenderThread = new Thread(blockSender);
|
||||
}
|
||||
|
||||
public void setTransportCallback(final Callback callback) {
|
||||
this.transportCallback = callback;
|
||||
}
|
||||
|
||||
public String getStreamId() {
|
||||
return this.streamId;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
if (initiator) {
|
||||
openInBandTransport();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CountDownLatch getTerminationLatch() {
|
||||
return this.terminationLatch;
|
||||
}
|
||||
|
||||
private void openInBandTransport() {
|
||||
final var iqPacket = new IqPacket(IqPacket.TYPE.SET);
|
||||
iqPacket.setTo(with);
|
||||
final var open = iqPacket.addChild("open", Namespace.IBB);
|
||||
open.setAttribute("block-size", this.blockSize);
|
||||
open.setAttribute("sid", this.streamId);
|
||||
Log.d(Config.LOGTAG, "sending ibb open");
|
||||
Log.d(Config.LOGTAG, iqPacket.toString());
|
||||
xmppConnection.sendIqPacket(iqPacket, this::receiveResponseToOpen);
|
||||
}
|
||||
|
||||
private void receiveResponseToOpen(final Account account, final IqPacket response) {
|
||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
Log.d(Config.LOGTAG, "ibb open was accepted");
|
||||
this.transportCallback.onTransportEstablished();
|
||||
this.blockSenderThread.start();
|
||||
} else {
|
||||
this.transportCallback.onTransportSetupFailed();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean deliverPacket(
|
||||
final PacketType packetType, final Jid from, final Element payload) {
|
||||
if (from == null || !from.equals(with)) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"ibb packet received from wrong address. was " + from + " expected " + with);
|
||||
return false;
|
||||
}
|
||||
return switch (packetType) {
|
||||
case OPEN -> receiveOpen();
|
||||
case DATA -> receiveData(payload.getContent());
|
||||
case CLOSE -> receiveClose();
|
||||
default -> throw new IllegalArgumentException("Invalid packet type");
|
||||
};
|
||||
}
|
||||
|
||||
private boolean receiveData(final String encoded) {
|
||||
final byte[] buffer;
|
||||
if (Strings.isNullOrEmpty(encoded)) {
|
||||
buffer = new byte[0];
|
||||
} else {
|
||||
buffer = BaseEncoding.base64().decode(encoded);
|
||||
}
|
||||
Log.d(Config.LOGTAG, "ibb received " + buffer.length + " bytes");
|
||||
try {
|
||||
pipedOutputStream.write(buffer);
|
||||
pipedOutputStream.flush();
|
||||
return true;
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to receive ibb data", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean receiveClose() {
|
||||
if (this.isReceiving.compareAndSet(true, false)) {
|
||||
try {
|
||||
this.pipedOutputStream.close();
|
||||
return true;
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "could not close pipedOutStream");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "received ibb close but was not receiving");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean receiveOpen() {
|
||||
Log.d(Config.LOGTAG, "receiveOpen()");
|
||||
if (this.isReceiving.get()) {
|
||||
Log.d(Config.LOGTAG, "ibb received open even though we were already open");
|
||||
return false;
|
||||
}
|
||||
this.isReceiving.set(true);
|
||||
transportCallback.onTransportEstablished();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void terminate() {
|
||||
// TODO send close
|
||||
Log.d(Config.LOGTAG, "IbbTransport.terminate()");
|
||||
this.terminationLatch.countDown();
|
||||
this.blockSender.close();
|
||||
this.blockSenderThread.interrupt();
|
||||
closeQuietly(this.pipedOutputStream);
|
||||
}
|
||||
|
||||
private static void closeQuietly(final OutputStream outputStream) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (final IOException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
final var outputStream = new PipedOutputStream();
|
||||
this.pipedInputStream.connect(outputStream);
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
final var inputStream = new PipedInputStream();
|
||||
this.pipedOutputStream.connect(inputStream);
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<TransportInfo> asTransportInfo() {
|
||||
return Futures.immediateFuture(
|
||||
new TransportInfo(new IbbTransportInfo(streamId, blockSize), null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<InitialTransportInfo> asInitialTransportInfo() {
|
||||
return Futures.immediateFuture(
|
||||
new InitialTransportInfo(
|
||||
UUID.randomUUID().toString(),
|
||||
new IbbTransportInfo(streamId, blockSize),
|
||||
null));
|
||||
}
|
||||
|
||||
public void setPeerBlockSize(long peerBlockSize) {
|
||||
this.blockSize = Math.min(Ints.saturatedCast(peerBlockSize), DEFAULT_BLOCK_SIZE);
|
||||
if (this.blockSize < DEFAULT_BLOCK_SIZE) {
|
||||
Log.d(Config.LOGTAG, "peer reconfigured IBB block size to " + this.blockSize);
|
||||
}
|
||||
this.blockSender.setBlockSize(this.blockSize);
|
||||
}
|
||||
|
||||
private static class BlockSender implements Runnable, Closeable {
|
||||
|
||||
private final XmppConnection xmppConnection;
|
||||
|
||||
private final Jid with;
|
||||
private final String streamId;
|
||||
|
||||
private int blockSize;
|
||||
private final PipedInputStream inputStream;
|
||||
private final Semaphore semaphore = new Semaphore(3);
|
||||
private final AtomicInteger sequencer = new AtomicInteger();
|
||||
private final AtomicBoolean isSending = new AtomicBoolean(true);
|
||||
|
||||
private BlockSender(
|
||||
XmppConnection xmppConnection,
|
||||
final Jid with,
|
||||
String streamId,
|
||||
int blockSize,
|
||||
PipedInputStream inputStream) {
|
||||
this.xmppConnection = xmppConnection;
|
||||
this.with = with;
|
||||
this.streamId = streamId;
|
||||
this.blockSize = blockSize;
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final var buffer = new byte[blockSize];
|
||||
try {
|
||||
while (isSending.get()) {
|
||||
final int count = this.inputStream.read(buffer);
|
||||
if (count < 0) {
|
||||
Log.d(Config.LOGTAG, "block sender reached EOF");
|
||||
return;
|
||||
}
|
||||
this.semaphore.acquire();
|
||||
final var block = new byte[count];
|
||||
System.arraycopy(buffer, 0, block, 0, block.length);
|
||||
sendIbbBlock(sequencer.getAndIncrement(), block);
|
||||
}
|
||||
} catch (final InterruptedException | InterruptedIOException e) {
|
||||
if (isSending.get()) {
|
||||
Log.w(Config.LOGTAG, "IbbBlockSender got interrupted while sending", e);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "block sender terminated", e);
|
||||
} finally {
|
||||
Closeables.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendIbbBlock(final int sequence, final byte[] block) {
|
||||
Log.d(Config.LOGTAG, "sending ibb block #" + sequence + " " + block.length + " bytes");
|
||||
final var iqPacket = new IqPacket(IqPacket.TYPE.SET);
|
||||
iqPacket.setTo(with);
|
||||
final var data = iqPacket.addChild("data", Namespace.IBB);
|
||||
data.setAttribute("sid", this.streamId);
|
||||
data.setAttribute("seq", sequence);
|
||||
data.setContent(BaseEncoding.base64().encode(block));
|
||||
this.xmppConnection.sendIqPacket(
|
||||
iqPacket,
|
||||
(a, response) -> {
|
||||
if (response.getType() != IqPacket.TYPE.RESULT) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"received iq error in response to data block #" + sequence);
|
||||
isSending.set(false);
|
||||
}
|
||||
semaphore.release();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.isSending.set(false);
|
||||
}
|
||||
|
||||
public void setBlockSize(final int blockSize) {
|
||||
this.blockSize = blockSize;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PacketType {
|
||||
OPEN,
|
||||
DATA,
|
||||
CLOSE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,870 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.transports;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.SocksSocketFactory;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class SocksByteStreamsTransport implements Transport {
|
||||
|
||||
private final XmppConnection xmppConnection;
|
||||
|
||||
private final AbstractJingleConnection.Id id;
|
||||
|
||||
private final boolean initiator;
|
||||
private final boolean useTor;
|
||||
|
||||
private final String streamId;
|
||||
|
||||
private ImmutableList<Candidate> theirCandidates;
|
||||
private final String theirDestination;
|
||||
private final SettableFuture<Connection> selectedByThemCandidate = SettableFuture.create();
|
||||
private final SettableFuture<String> theirProxyActivation = SettableFuture.create();
|
||||
|
||||
private final CountDownLatch terminationLatch = new CountDownLatch(1);
|
||||
|
||||
private final ConnectionProvider connectionProvider;
|
||||
private final ListenableFuture<Connection> ourProxyConnection;
|
||||
|
||||
private Connection connection;
|
||||
|
||||
private Callback transportCallback;
|
||||
|
||||
public SocksByteStreamsTransport(
|
||||
final XmppConnection xmppConnection,
|
||||
final AbstractJingleConnection.Id id,
|
||||
final boolean initiator,
|
||||
final boolean useTor,
|
||||
final String streamId,
|
||||
final Collection<Candidate> theirCandidates) {
|
||||
this.xmppConnection = xmppConnection;
|
||||
this.id = id;
|
||||
this.initiator = initiator;
|
||||
this.useTor = useTor;
|
||||
this.streamId = streamId;
|
||||
this.theirDestination =
|
||||
Hashing.sha1()
|
||||
.hashString(
|
||||
Joiner.on("")
|
||||
.join(
|
||||
Arrays.asList(
|
||||
streamId,
|
||||
id.with.toEscapedString(),
|
||||
id.account.getJid().toEscapedString())),
|
||||
StandardCharsets.UTF_8)
|
||||
.toString();
|
||||
final var ourDestination =
|
||||
Hashing.sha1()
|
||||
.hashString(
|
||||
Joiner.on("")
|
||||
.join(
|
||||
Arrays.asList(
|
||||
streamId,
|
||||
id.account.getJid().toEscapedString(),
|
||||
id.with.toEscapedString())),
|
||||
StandardCharsets.UTF_8)
|
||||
.toString();
|
||||
|
||||
this.connectionProvider =
|
||||
new ConnectionProvider(id.account.getJid(), ourDestination, useTor);
|
||||
new Thread(connectionProvider).start();
|
||||
this.ourProxyConnection = getOurProxyConnection(ourDestination);
|
||||
setTheirCandidates(theirCandidates);
|
||||
}
|
||||
|
||||
public SocksByteStreamsTransport(
|
||||
final XmppConnection xmppConnection,
|
||||
final AbstractJingleConnection.Id id,
|
||||
final boolean initiator,
|
||||
final boolean useTor) {
|
||||
this(
|
||||
xmppConnection,
|
||||
id,
|
||||
initiator,
|
||||
useTor,
|
||||
UUID.randomUUID().toString(),
|
||||
Collections.emptyList());
|
||||
}
|
||||
|
||||
public void connectTheirCandidates() {
|
||||
Preconditions.checkState(
|
||||
this.transportCallback != null, "transport callback needs to be set");
|
||||
// TODO this needs to go into a variable so we can cancel it
|
||||
final var connectionFinder =
|
||||
new ConnectionFinder(theirCandidates, theirDestination, useTor);
|
||||
new Thread(connectionFinder).start();
|
||||
Futures.addCallback(
|
||||
connectionFinder.connectionFuture,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(final Connection connection) {
|
||||
final Candidate candidate = connection.candidate;
|
||||
transportCallback.onCandidateUsed(streamId, candidate);
|
||||
establishTransport(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull final Throwable throwable) {
|
||||
if (throwable instanceof CandidateErrorException) {
|
||||
transportCallback.onCandidateError(streamId);
|
||||
}
|
||||
establishTransport(null);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private void establishTransport(final Connection selectedByUs) {
|
||||
Futures.addCallback(
|
||||
selectedByThemCandidate,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(Connection result) {
|
||||
establishTransport(selectedByUs, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Throwable throwable) {
|
||||
establishTransport(selectedByUs, null);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private void establishTransport(
|
||||
final Connection selectedByUs, final Connection selectedByThem) {
|
||||
final var selection = selectConnection(selectedByUs, selectedByThem);
|
||||
if (selection == null) {
|
||||
transportCallback.onTransportSetupFailed();
|
||||
return;
|
||||
}
|
||||
if (selection.connection.candidate.type == CandidateType.DIRECT) {
|
||||
Log.d(Config.LOGTAG, "final selection " + selection.connection.candidate);
|
||||
this.connection = selection.connection;
|
||||
this.transportCallback.onTransportEstablished();
|
||||
} else {
|
||||
final ListenableFuture<String> proxyActivation;
|
||||
if (selection.owner == Owner.THEIRS) {
|
||||
proxyActivation = this.theirProxyActivation;
|
||||
} else {
|
||||
proxyActivation = activateProxy(selection.connection.candidate);
|
||||
}
|
||||
Log.d(Config.LOGTAG, "waiting for proxy activation");
|
||||
Futures.addCallback(
|
||||
proxyActivation,
|
||||
new FutureCallback<>() {
|
||||
@Override
|
||||
public void onSuccess(final String cid) {
|
||||
// TODO compare cid to selection.connection.candidate
|
||||
connection = selection.connection;
|
||||
transportCallback.onTransportEstablished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Throwable throwable) {
|
||||
Log.d(Config.LOGTAG, "failed to activate proxy");
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionWithOwner selectConnection(
|
||||
final Connection selectedByUs, final Connection selectedByThem) {
|
||||
if (selectedByUs != null && selectedByThem != null) {
|
||||
if (selectedByUs.candidate.priority == selectedByThem.candidate.priority) {
|
||||
return initiator
|
||||
? new ConnectionWithOwner(selectedByUs, Owner.THEIRS)
|
||||
: new ConnectionWithOwner(selectedByThem, Owner.OURS);
|
||||
} else if (selectedByUs.candidate.priority > selectedByThem.candidate.priority) {
|
||||
return new ConnectionWithOwner(selectedByUs, Owner.THEIRS);
|
||||
} else {
|
||||
return new ConnectionWithOwner(selectedByThem, Owner.OURS);
|
||||
}
|
||||
}
|
||||
if (selectedByUs != null) {
|
||||
return new ConnectionWithOwner(selectedByUs, Owner.THEIRS);
|
||||
}
|
||||
if (selectedByThem != null) {
|
||||
return new ConnectionWithOwner(selectedByThem, Owner.OURS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ListenableFuture<String> activateProxy(final Candidate candidate) {
|
||||
Log.d(Config.LOGTAG, "trying to activate our proxy " + candidate);
|
||||
final SettableFuture<String> iqFuture = SettableFuture.create();
|
||||
final IqPacket proxyActivation = new IqPacket(IqPacket.TYPE.SET);
|
||||
proxyActivation.setTo(candidate.jid);
|
||||
final Element query = proxyActivation.addChild("query", Namespace.BYTE_STREAMS);
|
||||
query.setAttribute("sid", this.streamId);
|
||||
final Element activate = query.addChild("activate");
|
||||
activate.setContent(id.with.toEscapedString());
|
||||
xmppConnection.sendIqPacket(
|
||||
proxyActivation,
|
||||
(a, response) -> {
|
||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
Log.d(Config.LOGTAG, "our proxy has been activated");
|
||||
transportCallback.onProxyActivated(this.streamId, candidate);
|
||||
iqFuture.set(candidate.cid);
|
||||
} else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||
iqFuture.setException(new TimeoutException());
|
||||
} else {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
a.getJid().asBareJid()
|
||||
+ ": failed to activate proxy on "
|
||||
+ candidate.jid);
|
||||
iqFuture.setException(new IllegalStateException("Proxy activation failed"));
|
||||
}
|
||||
});
|
||||
return iqFuture;
|
||||
}
|
||||
|
||||
private ListenableFuture<Connection> getOurProxyConnection(final String ourDestination) {
|
||||
final var proxyFuture = getProxyCandidate();
|
||||
return Futures.transformAsync(
|
||||
proxyFuture,
|
||||
proxy -> {
|
||||
final var connectionFinder =
|
||||
new ConnectionFinder(ImmutableList.of(proxy), ourDestination, useTor);
|
||||
new Thread(connectionFinder).start();
|
||||
return Futures.transform(
|
||||
connectionFinder.connectionFuture,
|
||||
c -> {
|
||||
try {
|
||||
c.socket.setKeepAlive(true);
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"set keep alive on our own proxy connection");
|
||||
} catch (final SocketException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return c;
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private ListenableFuture<Candidate> getProxyCandidate() {
|
||||
if (Config.DISABLE_PROXY_LOOKUP) {
|
||||
return Futures.immediateFailedFuture(
|
||||
new IllegalStateException("Proxy look up is disabled"));
|
||||
}
|
||||
final Jid streamer = xmppConnection.findDiscoItemByFeature(Namespace.BYTE_STREAMS);
|
||||
if (streamer == null) {
|
||||
return Futures.immediateFailedFuture(
|
||||
new IllegalStateException("No proxy/streamer found"));
|
||||
}
|
||||
final IqPacket iqRequest = new IqPacket(IqPacket.TYPE.GET);
|
||||
iqRequest.setTo(streamer);
|
||||
iqRequest.query(Namespace.BYTE_STREAMS);
|
||||
final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
|
||||
xmppConnection.sendIqPacket(
|
||||
iqRequest,
|
||||
(a, response) -> {
|
||||
if (response.getType() == IqPacket.TYPE.RESULT) {
|
||||
final Element query = response.findChild("query", Namespace.BYTE_STREAMS);
|
||||
final Element streamHost =
|
||||
query == null
|
||||
? null
|
||||
: query.findChild("streamhost", Namespace.BYTE_STREAMS);
|
||||
final String host =
|
||||
streamHost == null ? null : streamHost.getAttribute("host");
|
||||
final Integer port =
|
||||
Ints.tryParse(
|
||||
Strings.nullToEmpty(
|
||||
streamHost == null
|
||||
? null
|
||||
: streamHost.getAttribute("port")));
|
||||
if (Strings.isNullOrEmpty(host) || port == null) {
|
||||
candidateFuture.setException(
|
||||
new IOException("Proxy response is missing attributes"));
|
||||
return;
|
||||
}
|
||||
candidateFuture.set(
|
||||
new Candidate(
|
||||
UUID.randomUUID().toString(),
|
||||
host,
|
||||
streamer,
|
||||
port,
|
||||
655360 + (initiator ? 0 : 15),
|
||||
CandidateType.PROXY));
|
||||
|
||||
} else if (response.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||
candidateFuture.setException(new TimeoutException());
|
||||
} else {
|
||||
candidateFuture.setException(
|
||||
new IOException(
|
||||
"received iq error in response to proxy discovery"));
|
||||
}
|
||||
});
|
||||
return candidateFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
final var connection = this.connection;
|
||||
if (connection == null) {
|
||||
throw new IOException("No candidate has been selected yet");
|
||||
}
|
||||
return connection.socket.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
final var connection = this.connection;
|
||||
if (connection == null) {
|
||||
throw new IOException("No candidate has been selected yet");
|
||||
}
|
||||
return connection.socket.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<TransportInfo> asTransportInfo() {
|
||||
final ListenableFuture<Collection<Connection>> proxyConnections =
|
||||
getOurProxyConnectionsFuture();
|
||||
return Futures.transform(
|
||||
proxyConnections,
|
||||
proxies -> {
|
||||
final var candidateBuilder = new ImmutableList.Builder<Candidate>();
|
||||
candidateBuilder.addAll(this.connectionProvider.candidates);
|
||||
candidateBuilder.addAll(Collections2.transform(proxies, p -> p.candidate));
|
||||
final var transportInfo =
|
||||
new SocksByteStreamsTransportInfo(
|
||||
this.streamId, candidateBuilder.build());
|
||||
return new TransportInfo(transportInfo, null);
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<InitialTransportInfo> asInitialTransportInfo() {
|
||||
return Futures.transform(
|
||||
asTransportInfo(),
|
||||
ti ->
|
||||
new InitialTransportInfo(
|
||||
UUID.randomUUID().toString(), ti.transportInfo, ti.group),
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private ListenableFuture<Collection<Connection>> getOurProxyConnectionsFuture() {
|
||||
return Futures.catching(
|
||||
Futures.transform(
|
||||
this.ourProxyConnection,
|
||||
Collections::singleton,
|
||||
MoreExecutors.directExecutor()),
|
||||
Exception.class,
|
||||
ex -> {
|
||||
Log.d(Config.LOGTAG, "could not find a proxy of our own", ex);
|
||||
return Collections.emptyList();
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private Collection<Connection> getOurProxyConnections() {
|
||||
final var future = getOurProxyConnectionsFuture();
|
||||
if (future.isDone()) {
|
||||
try {
|
||||
return future.get();
|
||||
} catch (final Exception e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
Log.d(Config.LOGTAG, "terminating socks transport");
|
||||
this.terminationLatch.countDown();
|
||||
final var connection = this.connection;
|
||||
if (connection != null) {
|
||||
closeSocket(connection.socket);
|
||||
}
|
||||
this.connectionProvider.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransportCallback(final Callback callback) {
|
||||
this.transportCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {
|
||||
this.connectTheirCandidates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CountDownLatch getTerminationLatch() {
|
||||
return this.terminationLatch;
|
||||
}
|
||||
|
||||
public boolean setCandidateUsed(final String cid) {
|
||||
final var ourProxyConnections = getOurProxyConnections();
|
||||
final var proxyConnection =
|
||||
Iterables.tryFind(ourProxyConnections, c -> c.candidate.cid.equals(cid));
|
||||
if (proxyConnection.isPresent()) {
|
||||
this.selectedByThemCandidate.set(proxyConnection.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
// the peer selected a connection that is not our proxy. so we can close our proxies
|
||||
closeConnections(ourProxyConnections);
|
||||
|
||||
final var connection = this.connectionProvider.findPeerConnection(cid);
|
||||
if (connection.isPresent()) {
|
||||
this.selectedByThemCandidate.set(connection.get());
|
||||
return true;
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "none of the connected candidates has cid " + cid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCandidateError() {
|
||||
this.selectedByThemCandidate.setException(
|
||||
new CandidateErrorException("Remote could not connect to any of our candidates"));
|
||||
}
|
||||
|
||||
public void setProxyActivated(final String cid) {
|
||||
this.theirProxyActivation.set(cid);
|
||||
}
|
||||
|
||||
public void setProxyError() {
|
||||
this.theirProxyActivation.setException(
|
||||
new IllegalStateException("Remote could not activate their proxy"));
|
||||
}
|
||||
|
||||
public void setTheirCandidates(Collection<Candidate> candidates) {
|
||||
this.theirCandidates =
|
||||
Ordering.from(
|
||||
(Comparator<Candidate>)
|
||||
(o1, o2) -> Integer.compare(o2.priority, o1.priority))
|
||||
.immutableSortedCopy(candidates);
|
||||
}
|
||||
|
||||
private static void closeSocket(final Socket socket) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (final IOException e) {
|
||||
Log.w(Config.LOGTAG, "error closing socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConnectionProvider implements Runnable {
|
||||
|
||||
private final ExecutorService clientConnectionExecutorService =
|
||||
Executors.newFixedThreadPool(4);
|
||||
|
||||
private final ImmutableList<Candidate> candidates;
|
||||
|
||||
private final int port;
|
||||
|
||||
private final AtomicBoolean acceptingConnections = new AtomicBoolean(true);
|
||||
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
private final String destination;
|
||||
|
||||
private final ArrayList<Connection> peerConnections = new ArrayList<>();
|
||||
|
||||
private ConnectionProvider(
|
||||
final Jid account, final String destination, final boolean useTor) {
|
||||
final SecureRandom secureRandom = new SecureRandom();
|
||||
this.port = secureRandom.nextInt(60_000) + 1024;
|
||||
this.destination = destination;
|
||||
final InetAddress[] localAddresses;
|
||||
if (Config.USE_DIRECT_JINGLE_CANDIDATES && !useTor) {
|
||||
localAddresses =
|
||||
DirectConnectionUtils.getLocalAddresses().toArray(new InetAddress[0]);
|
||||
} else {
|
||||
localAddresses = new InetAddress[0];
|
||||
}
|
||||
final var candidateBuilder = new ImmutableList.Builder<Candidate>();
|
||||
for (int i = 0; i < localAddresses.length; ++i) {
|
||||
final var inetAddress = localAddresses[i];
|
||||
candidateBuilder.add(
|
||||
new Candidate(
|
||||
UUID.randomUUID().toString(),
|
||||
inetAddress.getHostAddress(),
|
||||
account,
|
||||
port,
|
||||
8257536 + i,
|
||||
CandidateType.DIRECT));
|
||||
}
|
||||
this.candidates = candidateBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (this.candidates.isEmpty()) {
|
||||
Log.d(Config.LOGTAG, "no direct candidates. stopping ConnectionProvider");
|
||||
return;
|
||||
}
|
||||
try (final ServerSocket serverSocket = new ServerSocket(this.port)) {
|
||||
this.serverSocket = serverSocket;
|
||||
while (acceptingConnections.get()) {
|
||||
final Socket clientSocket;
|
||||
try {
|
||||
clientSocket = serverSocket.accept();
|
||||
} catch (final SocketException ignored) {
|
||||
Log.d(Config.LOGTAG, "server socket has been closed.");
|
||||
return;
|
||||
}
|
||||
clientConnectionExecutorService.execute(
|
||||
() -> acceptClientConnection(clientSocket));
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "could not create server socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptClientConnection(final Socket socket) {
|
||||
final var localAddress = socket.getLocalAddress();
|
||||
final var hostAddress = localAddress == null ? null : localAddress.getHostAddress();
|
||||
final var candidate =
|
||||
Iterables.tryFind(this.candidates, c -> c.host.equals(hostAddress));
|
||||
if (candidate.isPresent()) {
|
||||
acceptingConnections(socket, candidate.get());
|
||||
|
||||
} else {
|
||||
closeSocket(socket);
|
||||
Log.d(Config.LOGTAG, "no local candidate found for connection on " + hostAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptingConnections(final Socket socket, final Candidate candidate) {
|
||||
final var remoteAddress = socket.getRemoteSocketAddress();
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"accepted client connection from " + remoteAddress + " to " + candidate);
|
||||
try {
|
||||
socket.setSoTimeout(3000);
|
||||
final byte[] authBegin = new byte[2];
|
||||
final InputStream inputStream = socket.getInputStream();
|
||||
final OutputStream outputStream = socket.getOutputStream();
|
||||
ByteStreams.readFully(inputStream, authBegin);
|
||||
if (authBegin[0] != 0x5) {
|
||||
socket.close();
|
||||
}
|
||||
final short methodCount = authBegin[1];
|
||||
final byte[] methods = new byte[methodCount];
|
||||
ByteStreams.readFully(inputStream, methods);
|
||||
if (SocksSocketFactory.contains((byte) 0x00, methods)) {
|
||||
outputStream.write(new byte[] {0x05, 0x00});
|
||||
} else {
|
||||
outputStream.write(new byte[] {0x05, (byte) 0xff});
|
||||
}
|
||||
final byte[] connectCommand = new byte[4];
|
||||
ByteStreams.readFully(inputStream, connectCommand);
|
||||
if (connectCommand[0] == 0x05
|
||||
&& connectCommand[1] == 0x01
|
||||
&& connectCommand[3] == 0x03) {
|
||||
int destinationCount = inputStream.read();
|
||||
final byte[] destination = new byte[destinationCount];
|
||||
ByteStreams.readFully(inputStream, destination);
|
||||
final byte[] port = new byte[2];
|
||||
ByteStreams.readFully(inputStream, port);
|
||||
final String receivedDestination = new String(destination);
|
||||
final ByteBuffer response = ByteBuffer.allocate(7 + destination.length);
|
||||
final byte[] responseHeader;
|
||||
final boolean success;
|
||||
if (receivedDestination.equals(this.destination)) {
|
||||
responseHeader = new byte[] {0x05, 0x00, 0x00, 0x03};
|
||||
synchronized (this.peerConnections) {
|
||||
peerConnections.add(new Connection(candidate, socket));
|
||||
}
|
||||
success = true;
|
||||
} else {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
"destination mismatch. received "
|
||||
+ receivedDestination
|
||||
+ " (expected "
|
||||
+ this.destination
|
||||
+ ")");
|
||||
responseHeader = new byte[] {0x05, 0x04, 0x00, 0x03};
|
||||
success = false;
|
||||
}
|
||||
response.put(responseHeader);
|
||||
response.put((byte) destination.length);
|
||||
response.put(destination);
|
||||
response.put(port);
|
||||
outputStream.write(response.array());
|
||||
outputStream.flush();
|
||||
if (success) {
|
||||
Log.d(
|
||||
Config.LOGTAG,
|
||||
remoteAddress + " successfully connected to " + candidate);
|
||||
} else {
|
||||
closeSocket(socket);
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "failed to accept client connection to " + candidate, e);
|
||||
closeSocket(socket);
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeServerSocket(@Nullable final ServerSocket serverSocket) {
|
||||
if (serverSocket == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
serverSocket.close();
|
||||
} catch (final IOException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Connection> findPeerConnection(String cid) {
|
||||
synchronized (this.peerConnections) {
|
||||
return Iterables.tryFind(
|
||||
this.peerConnections, connection -> connection.candidate.cid.equals(cid));
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.acceptingConnections.set(false); // we have probably done this earlier already
|
||||
closeServerSocket(this.serverSocket);
|
||||
synchronized (this.peerConnections) {
|
||||
closeConnections(this.peerConnections);
|
||||
this.peerConnections.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeConnections(final Iterable<Connection> connections) {
|
||||
for (final var connection : connections) {
|
||||
closeSocket(connection.socket);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConnectionFinder implements Runnable {
|
||||
|
||||
private final SettableFuture<Connection> connectionFuture = SettableFuture.create();
|
||||
|
||||
private final ImmutableList<Candidate> candidates;
|
||||
private final String destination;
|
||||
private final boolean useTor;
|
||||
|
||||
private ConnectionFinder(
|
||||
final ImmutableList<Candidate> candidates,
|
||||
final String destination,
|
||||
final boolean useTor) {
|
||||
this.candidates = candidates;
|
||||
this.destination = destination;
|
||||
this.useTor = useTor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (final Candidate candidate : this.candidates) {
|
||||
// TODO we can check if there is already something in `selectedByThemCandidate` with
|
||||
// a higher priority and abort
|
||||
try {
|
||||
connectionFuture.set(connect(candidate));
|
||||
Log.d(Config.LOGTAG, "connected to " + candidate);
|
||||
return;
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "could not connect to candidate " + candidate);
|
||||
}
|
||||
}
|
||||
connectionFuture.setException(
|
||||
new CandidateErrorException(
|
||||
String.format(
|
||||
Locale.US,
|
||||
"Gave up after %d candidates",
|
||||
this.candidates.size())));
|
||||
}
|
||||
|
||||
private Connection connect(final Candidate candidate) throws IOException {
|
||||
final var timeout = 3000;
|
||||
final Socket socket;
|
||||
if (useTor) {
|
||||
Log.d(Config.LOGTAG, "using Tor to connect to candidate " + candidate.host);
|
||||
socket = SocksSocketFactory.createSocketOverTor(candidate.host, candidate.port);
|
||||
} else {
|
||||
socket = new Socket();
|
||||
final SocketAddress address = new InetSocketAddress(candidate.host, candidate.port);
|
||||
socket.connect(address, timeout);
|
||||
}
|
||||
socket.setSoTimeout(timeout);
|
||||
SocksSocketFactory.createSocksConnection(socket, destination, 0);
|
||||
socket.setSoTimeout(0);
|
||||
return new Connection(candidate, socket);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CandidateErrorException extends IllegalStateException {
|
||||
private CandidateErrorException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Owner {
|
||||
THEIRS,
|
||||
OURS
|
||||
}
|
||||
|
||||
public static class ConnectionWithOwner {
|
||||
public final Connection connection;
|
||||
public final Owner owner;
|
||||
|
||||
public ConnectionWithOwner(Connection connection, Owner owner) {
|
||||
this.connection = connection;
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Connection {
|
||||
|
||||
public final Candidate candidate;
|
||||
public final Socket socket;
|
||||
|
||||
public Connection(Candidate candidate, Socket socket) {
|
||||
this.candidate = candidate;
|
||||
this.socket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Candidate implements Transport.Candidate {
|
||||
public final String cid;
|
||||
public final String host;
|
||||
public final Jid jid;
|
||||
public final int port;
|
||||
public final int priority;
|
||||
public final CandidateType type;
|
||||
|
||||
public Candidate(
|
||||
final String cid,
|
||||
final String host,
|
||||
final Jid jid,
|
||||
int port,
|
||||
int priority,
|
||||
final CandidateType type) {
|
||||
this.cid = cid;
|
||||
this.host = host;
|
||||
this.jid = jid;
|
||||
this.port = port;
|
||||
this.priority = priority;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public static Candidate of(final Element element) {
|
||||
Preconditions.checkArgument(
|
||||
"candidate".equals(element.getName()),
|
||||
"trying to construct candidate from non candidate element");
|
||||
Preconditions.checkArgument(
|
||||
Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()),
|
||||
"candidate element is in correct namespace");
|
||||
final String cid = element.getAttribute("cid");
|
||||
final String host = element.getAttribute("host");
|
||||
final String jid = element.getAttribute("jid");
|
||||
final String port = element.getAttribute("port");
|
||||
final String priority = element.getAttribute("priority");
|
||||
final String type = element.getAttribute("type");
|
||||
if (Strings.isNullOrEmpty(cid)
|
||||
|| Strings.isNullOrEmpty(host)
|
||||
|| Strings.isNullOrEmpty(jid)
|
||||
|| Strings.isNullOrEmpty(port)
|
||||
|| Strings.isNullOrEmpty(priority)
|
||||
|| Strings.isNullOrEmpty(type)) {
|
||||
throw new IllegalArgumentException("Candidate is missing non optional attribute");
|
||||
}
|
||||
return new Candidate(
|
||||
cid,
|
||||
host,
|
||||
Jid.ofEscaped(jid),
|
||||
Integer.parseInt(port),
|
||||
Integer.parseInt(priority),
|
||||
CandidateType.valueOf(type.toUpperCase(Locale.ROOT)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
.add("cid", cid)
|
||||
.add("host", host)
|
||||
.add("jid", jid)
|
||||
.add("port", port)
|
||||
.add("priority", priority)
|
||||
.add("type", type)
|
||||
.toString();
|
||||
}
|
||||
|
||||
public Element asElement() {
|
||||
final var element = new Element("candidate", Namespace.JINGLE_TRANSPORTS_S5B);
|
||||
element.setAttribute("cid", this.cid);
|
||||
element.setAttribute("host", this.host);
|
||||
element.setAttribute("jid", this.jid);
|
||||
element.setAttribute("port", this.port);
|
||||
element.setAttribute("priority", this.priority);
|
||||
element.setAttribute("type", this.type.toString().toLowerCase(Locale.ROOT));
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CandidateType {
|
||||
DIRECT,
|
||||
PROXY
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.transports;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public interface Transport {
|
||||
|
||||
OutputStream getOutputStream() throws IOException;
|
||||
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
ListenableFuture<TransportInfo> asTransportInfo();
|
||||
|
||||
ListenableFuture<InitialTransportInfo> asInitialTransportInfo();
|
||||
|
||||
default void readyToSentAdditionalCandidates() {}
|
||||
|
||||
void terminate();
|
||||
|
||||
void setTransportCallback(final Callback callback);
|
||||
|
||||
void connect();
|
||||
|
||||
CountDownLatch getTerminationLatch();
|
||||
|
||||
interface Callback {
|
||||
void onTransportEstablished();
|
||||
|
||||
void onTransportSetupFailed();
|
||||
|
||||
void onAdditionalCandidate(final String contentName, final Candidate candidate);
|
||||
|
||||
void onCandidateUsed(String streamId, SocksByteStreamsTransport.Candidate candidate);
|
||||
|
||||
void onCandidateError(String streamId);
|
||||
|
||||
void onProxyActivated(String streamId, SocksByteStreamsTransport.Candidate candidate);
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
SEND,
|
||||
RECEIVE,
|
||||
SEND_RECEIVE
|
||||
}
|
||||
|
||||
class InitialTransportInfo extends TransportInfo {
|
||||
public final String contentName;
|
||||
|
||||
public InitialTransportInfo(
|
||||
String contentName, GenericTransportInfo transportInfo, Group group) {
|
||||
super(transportInfo, group);
|
||||
this.contentName = contentName;
|
||||
}
|
||||
}
|
||||
|
||||
class TransportInfo {
|
||||
|
||||
public final GenericTransportInfo transportInfo;
|
||||
public final Group group;
|
||||
|
||||
public TransportInfo(final GenericTransportInfo transportInfo, final Group group) {
|
||||
this.transportInfo = transportInfo;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public TransportInfo(final GenericTransportInfo transportInfo) {
|
||||
this.transportInfo = transportInfo;
|
||||
this.group = null;
|
||||
}
|
||||
}
|
||||
|
||||
interface Candidate {}
|
||||
}
|
|
@ -0,0 +1,617 @@
|
|||
package eu.siacs.conversations.xmpp.jingle.transports;
|
||||
|
||||
import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.buildConfiguration;
|
||||
import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Closeables;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xml.Namespace;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.IceServers;
|
||||
import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import org.webrtc.CandidatePairChangeEvent;
|
||||
import org.webrtc.DataChannel;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.SessionDescription;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class WebRTCDataChannelTransport implements Transport {
|
||||
|
||||
private static final int BUFFER_SIZE = 16_384;
|
||||
private static final int MAX_SENT_BUFFER = 256 * 1024;
|
||||
|
||||
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||
private final ExecutorService localDescriptionExecutorService =
|
||||
Executors.newSingleThreadExecutor();
|
||||
|
||||
private final AtomicBoolean readyToSentIceCandidates = new AtomicBoolean(false);
|
||||
private final Queue<IceCandidate> pendingOutgoingIceCandidates = new LinkedList<>();
|
||||
|
||||
private final PipedOutputStream pipedOutputStream = new PipedOutputStream();
|
||||
private final WritableByteChannel writableByteChannel = Channels.newChannel(pipedOutputStream);
|
||||
private final PipedInputStream pipedInputStream = new PipedInputStream(BUFFER_SIZE);
|
||||
|
||||
private final AtomicBoolean connected = new AtomicBoolean(false);
|
||||
|
||||
private final CountDownLatch terminationLatch = new CountDownLatch(1);
|
||||
|
||||
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
|
||||
|
||||
private final XmppConnection xmppConnection;
|
||||
private final Account account;
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private ListenableFuture<PeerConnection> peerConnectionFuture;
|
||||
|
||||
private ListenableFuture<SessionDescription> localDescriptionFuture;
|
||||
|
||||
private DataChannel dataChannel;
|
||||
|
||||
private Callback transportCallback;
|
||||
|
||||
private final PeerConnection.Observer peerConnectionObserver =
|
||||
new PeerConnection.Observer() {
|
||||
@Override
|
||||
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
|
||||
Log.d(Config.LOGTAG, "onSignalChange(" + signalingState + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionChange(final PeerConnection.PeerConnectionState state) {
|
||||
stateHistory.add(state);
|
||||
Log.d(Config.LOGTAG, "onConnectionChange(" + state + ")");
|
||||
if (state == PeerConnection.PeerConnectionState.CONNECTED) {
|
||||
if (connected.compareAndSet(false, true)) {
|
||||
executorService.execute(() -> onIceConnectionConnected());
|
||||
}
|
||||
}
|
||||
if (state == PeerConnection.PeerConnectionState.FAILED) {
|
||||
final boolean neverConnected =
|
||||
!stateHistory.contains(
|
||||
PeerConnection.PeerConnectionState.CONNECTED);
|
||||
// we want to terminate the connection a) to properly fail if a connection
|
||||
// drops during a transfer and b) to avoid race conditions if we find a
|
||||
// connection after failure while waiting for the initiator to replace
|
||||
// transport
|
||||
executorService.execute(() -> terminate());
|
||||
if (neverConnected) {
|
||||
executorService.execute(() -> onIceConnectionFailed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(
|
||||
final PeerConnection.IceConnectionState newState) {}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionReceivingChange(boolean b) {}
|
||||
|
||||
@Override
|
||||
public void onIceGatheringChange(
|
||||
final PeerConnection.IceGatheringState iceGatheringState) {
|
||||
Log.d(Config.LOGTAG, "onIceGatheringChange(" + iceGatheringState + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(final IceCandidate iceCandidate) {
|
||||
if (readyToSentIceCandidates.get()) {
|
||||
WebRTCDataChannelTransport.this.onIceCandidate(
|
||||
iceCandidate.sdpMid, iceCandidate.sdp);
|
||||
} else {
|
||||
pendingOutgoingIceCandidates.add(iceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}
|
||||
|
||||
@Override
|
||||
public void onAddStream(MediaStream mediaStream) {}
|
||||
|
||||
@Override
|
||||
public void onRemoveStream(MediaStream mediaStream) {}
|
||||
|
||||
@Override
|
||||
public void onDataChannel(final DataChannel dataChannel) {
|
||||
Log.d(Config.LOGTAG, "onDataChannel()");
|
||||
WebRTCDataChannelTransport.this.setDataChannel(dataChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenegotiationNeeded() {
|
||||
Log.d(Config.LOGTAG, "onRenegotiationNeeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
|
||||
Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote);
|
||||
Log.d(Config.LOGTAG, "local candidate selected: " + event.local);
|
||||
}
|
||||
};
|
||||
|
||||
private DataChannelWriter dataChannelWriter;
|
||||
|
||||
private void onIceConnectionConnected() {
|
||||
this.transportCallback.onTransportEstablished();
|
||||
}
|
||||
|
||||
private void onIceConnectionFailed() {
|
||||
this.transportCallback.onTransportSetupFailed();
|
||||
}
|
||||
|
||||
private void setDataChannel(final DataChannel dataChannel) {
|
||||
Log.d(Config.LOGTAG, "the 'receiving' data channel has id " + dataChannel.id());
|
||||
this.dataChannel = dataChannel;
|
||||
this.dataChannel.registerObserver(
|
||||
new OnMessageObserver() {
|
||||
@Override
|
||||
public void onMessage(final DataChannel.Buffer buffer) {
|
||||
Log.d(Config.LOGTAG, "onMessage() (the other one)");
|
||||
try {
|
||||
WebRTCDataChannelTransport.this.writableByteChannel.write(buffer.data);
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "error writing to output stream");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void onIceCandidate(final String mid, final String sdp) {
|
||||
final var candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(sdp, null);
|
||||
this.transportCallback.onAdditionalCandidate(mid, candidate);
|
||||
}
|
||||
|
||||
public WebRTCDataChannelTransport(
|
||||
final Context context,
|
||||
final XmppConnection xmppConnection,
|
||||
final Account account,
|
||||
final boolean initiator) {
|
||||
PeerConnectionFactory.initialize(
|
||||
PeerConnectionFactory.InitializationOptions.builder(context)
|
||||
.setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/")
|
||||
.createInitializationOptions());
|
||||
this.peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory();
|
||||
this.xmppConnection = xmppConnection;
|
||||
this.account = account;
|
||||
this.peerConnectionFuture =
|
||||
Futures.transform(
|
||||
getIceServers(),
|
||||
iceServers -> createPeerConnection(iceServers, true),
|
||||
MoreExecutors.directExecutor());
|
||||
if (initiator) {
|
||||
this.localDescriptionFuture = setLocalDescription();
|
||||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<List<PeerConnection.IceServer>> getIceServers() {
|
||||
if (Config.DISABLE_PROXY_LOOKUP) {
|
||||
return Futures.immediateFuture(Collections.emptyList());
|
||||
}
|
||||
if (xmppConnection.getFeatures().externalServiceDiscovery()) {
|
||||
final SettableFuture<List<PeerConnection.IceServer>> iceServerFuture =
|
||||
SettableFuture.create();
|
||||
final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
|
||||
request.setTo(this.account.getDomain());
|
||||
request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
|
||||
xmppConnection.sendIqPacket(
|
||||
request,
|
||||
(account, response) -> {
|
||||
final var iceServers = IceServers.parse(response);
|
||||
if (iceServers.size() == 0) {
|
||||
Log.w(
|
||||
Config.LOGTAG,
|
||||
account.getJid().asBareJid()
|
||||
+ ": no ICE server found "
|
||||
+ response);
|
||||
}
|
||||
iceServerFuture.set(iceServers);
|
||||
});
|
||||
return iceServerFuture;
|
||||
} else {
|
||||
return Futures.immediateFuture(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
private PeerConnection createPeerConnection(
|
||||
final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
|
||||
final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle);
|
||||
final PeerConnection peerConnection =
|
||||
requirePeerConnectionFactory()
|
||||
.createPeerConnection(rtcConfig, peerConnectionObserver);
|
||||
if (peerConnection == null) {
|
||||
throw new IllegalStateException("Unable to create PeerConnection");
|
||||
}
|
||||
final var dataChannelInit = new DataChannel.Init();
|
||||
dataChannelInit.protocol = "xmpp-jingle";
|
||||
final var dataChannel = peerConnection.createDataChannel("test", dataChannelInit);
|
||||
this.dataChannelWriter = new DataChannelWriter(this.pipedInputStream, dataChannel);
|
||||
Log.d(Config.LOGTAG, "the 'sending' data channel has id " + dataChannel.id());
|
||||
new Thread(this.dataChannelWriter).start();
|
||||
return peerConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
final var outputStream = new PipedOutputStream();
|
||||
this.pipedInputStream.connect(outputStream);
|
||||
this.dataChannelWriter.pipedInputStreamLatch.countDown();
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
final var inputStream = new PipedInputStream(BUFFER_SIZE);
|
||||
this.pipedOutputStream.connect(inputStream);
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<TransportInfo> asTransportInfo() {
|
||||
Preconditions.checkState(
|
||||
this.localDescriptionFuture != null,
|
||||
"Make sure you are setting initiator description first");
|
||||
return Futures.transform(
|
||||
asInitialTransportInfo(), info -> info, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<InitialTransportInfo> asInitialTransportInfo() {
|
||||
return Futures.transform(
|
||||
localDescriptionFuture,
|
||||
sdp ->
|
||||
WebRTCDataChannelTransportInfo.of(
|
||||
eu.siacs.conversations.xmpp.jingle.SessionDescription.parse(
|
||||
sdp.description)),
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readyToSentAdditionalCandidates() {
|
||||
readyToSentIceCandidates.set(true);
|
||||
while (this.pendingOutgoingIceCandidates.peek() != null) {
|
||||
final var candidate = pendingOutgoingIceCandidates.poll();
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
onIceCandidate(candidate.sdpMid, candidate.sdp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void terminate() {
|
||||
terminate(this.dataChannel);
|
||||
this.dataChannel = null;
|
||||
final var dataChannelWriter = this.dataChannelWriter;
|
||||
if (dataChannelWriter != null) {
|
||||
dataChannelWriter.close();
|
||||
}
|
||||
this.dataChannelWriter = null;
|
||||
final var future = this.peerConnectionFuture;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
try {
|
||||
final PeerConnection peerConnection = requirePeerConnection();
|
||||
terminate(peerConnection);
|
||||
} catch (final WebRTCWrapper.PeerConnectionNotInitialized e) {
|
||||
Log.d(Config.LOGTAG, "peer connection was not initialized during termination");
|
||||
}
|
||||
this.peerConnectionFuture = null;
|
||||
final var peerConnectionFactory = this.peerConnectionFactory;
|
||||
if (peerConnectionFactory != null) {
|
||||
peerConnectionFactory.dispose();
|
||||
}
|
||||
this.peerConnectionFactory = null;
|
||||
closeQuietly(this.pipedOutputStream);
|
||||
this.terminationLatch.countDown();
|
||||
Log.d(Config.LOGTAG, WebRTCDataChannelTransport.class.getSimpleName() + " terminated");
|
||||
}
|
||||
|
||||
private static void closeQuietly(final OutputStream outputStream) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (final IOException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static void terminate(final DataChannel dataChannel) {
|
||||
if (dataChannel == null) {
|
||||
Log.d(Config.LOGTAG, "nothing to terminate. data channel is already null");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dataChannel.close();
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.w(Config.LOGTAG, "could not close data channel");
|
||||
}
|
||||
try {
|
||||
dataChannel.dispose();
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.w(Config.LOGTAG, "could not dispose data channel");
|
||||
}
|
||||
}
|
||||
|
||||
private static void terminate(final PeerConnection peerConnection) {
|
||||
if (peerConnection == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
peerConnection.dispose();
|
||||
Log.d(Config.LOGTAG, "terminated peer connection!");
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.w(Config.LOGTAG, "could not dispose of peer connection");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransportCallback(final Callback callback) {
|
||||
this.transportCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {}
|
||||
|
||||
@Override
|
||||
public CountDownLatch getTerminationLatch() {
|
||||
return this.terminationLatch;
|
||||
}
|
||||
|
||||
synchronized ListenableFuture<SessionDescription> setLocalDescription() {
|
||||
return Futures.transformAsync(
|
||||
peerConnectionFuture,
|
||||
peerConnection -> {
|
||||
if (peerConnection == null) {
|
||||
return Futures.immediateFailedFuture(
|
||||
new IllegalStateException("PeerConnection was null"));
|
||||
}
|
||||
final SettableFuture<SessionDescription> future = SettableFuture.create();
|
||||
peerConnection.setLocalDescription(
|
||||
new WebRTCWrapper.SetSdpObserver() {
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
future.setFuture(getLocalDescriptionFuture(peerConnection));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(final String message) {
|
||||
future.setException(
|
||||
new WebRTCWrapper.FailureToSetDescriptionException(
|
||||
message));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private ListenableFuture<SessionDescription> getLocalDescriptionFuture(
|
||||
final PeerConnection peerConnection) {
|
||||
return Futures.submit(
|
||||
() -> {
|
||||
final SessionDescription description = peerConnection.getLocalDescription();
|
||||
WebRTCWrapper.logDescription(description);
|
||||
return description;
|
||||
},
|
||||
localDescriptionExecutorService);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private PeerConnectionFactory requirePeerConnectionFactory() {
|
||||
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
|
||||
if (peerConnectionFactory == null) {
|
||||
throw new IllegalStateException("Make sure PeerConnectionFactory is initialized");
|
||||
}
|
||||
return peerConnectionFactory;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private PeerConnection requirePeerConnection() {
|
||||
final var future = this.peerConnectionFuture;
|
||||
if (future != null && future.isDone()) {
|
||||
try {
|
||||
return future.get();
|
||||
} catch (final InterruptedException | ExecutionException e) {
|
||||
throw new WebRTCWrapper.PeerConnectionNotInitialized();
|
||||
}
|
||||
} else {
|
||||
throw new WebRTCWrapper.PeerConnectionNotInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<IceCandidate> iceCandidatesOf(
|
||||
final String contentName,
|
||||
final IceUdpTransportInfo.Credentials credentials,
|
||||
final List<IceUdpTransportInfo.Candidate> candidates) {
|
||||
final ImmutableList.Builder<IceCandidate> iceCandidateBuilder =
|
||||
new ImmutableList.Builder<>();
|
||||
for (final IceUdpTransportInfo.Candidate candidate : candidates) {
|
||||
final String sdp;
|
||||
try {
|
||||
sdp = candidate.toSdpAttribute(credentials.ufrag);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
continue;
|
||||
}
|
||||
// TODO mLneIndex should probably not be hard coded
|
||||
iceCandidateBuilder.add(new IceCandidate(contentName, 0, sdp));
|
||||
}
|
||||
return iceCandidateBuilder.build();
|
||||
}
|
||||
|
||||
public void addIceCandidates(final List<IceCandidate> iceCandidates) {
|
||||
try {
|
||||
for (final var candidate : iceCandidates) {
|
||||
requirePeerConnection().addIceCandidate(candidate);
|
||||
}
|
||||
} catch (WebRTCWrapper.PeerConnectionNotInitialized e) {
|
||||
Log.w(Config.LOGTAG, "could not add ice candidate. peer connection is not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
public void setInitiatorDescription(
|
||||
final eu.siacs.conversations.xmpp.jingle.SessionDescription sessionDescription) {
|
||||
final var sdp =
|
||||
new SessionDescription(
|
||||
SessionDescription.Type.OFFER, sessionDescription.toString());
|
||||
final var setFuture = setRemoteDescriptionFuture(sdp);
|
||||
this.localDescriptionFuture =
|
||||
Futures.transformAsync(
|
||||
setFuture, v -> setLocalDescription(), MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
public void setResponderDescription(
|
||||
final eu.siacs.conversations.xmpp.jingle.SessionDescription sessionDescription) {
|
||||
Log.d(Config.LOGTAG, "setResponder description");
|
||||
final var sdp =
|
||||
new SessionDescription(
|
||||
SessionDescription.Type.ANSWER, sessionDescription.toString());
|
||||
logDescription(sdp);
|
||||
setRemoteDescriptionFuture(sdp);
|
||||
}
|
||||
|
||||
synchronized ListenableFuture<Void> setRemoteDescriptionFuture(
|
||||
final SessionDescription sessionDescription) {
|
||||
return Futures.transformAsync(
|
||||
this.peerConnectionFuture,
|
||||
peerConnection -> {
|
||||
if (peerConnection == null) {
|
||||
return Futures.immediateFailedFuture(
|
||||
new IllegalStateException("PeerConnection was null"));
|
||||
}
|
||||
final SettableFuture<Void> future = SettableFuture.create();
|
||||
peerConnection.setRemoteDescription(
|
||||
new WebRTCWrapper.SetSdpObserver() {
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
future.set(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(final String message) {
|
||||
future.setException(
|
||||
new WebRTCWrapper.FailureToSetDescriptionException(
|
||||
message));
|
||||
}
|
||||
},
|
||||
sessionDescription);
|
||||
return future;
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private static class DataChannelWriter implements Runnable {
|
||||
|
||||
private final CountDownLatch pipedInputStreamLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch dataChannelLatch = new CountDownLatch(1);
|
||||
private final AtomicBoolean isSending = new AtomicBoolean(true);
|
||||
private final InputStream inputStream;
|
||||
private final DataChannel dataChannel;
|
||||
|
||||
private DataChannelWriter(InputStream inputStream, DataChannel dataChannel) {
|
||||
this.inputStream = inputStream;
|
||||
this.dataChannel = dataChannel;
|
||||
final StateChangeObserver stateChangeObserver =
|
||||
new StateChangeObserver() {
|
||||
|
||||
@Override
|
||||
public void onStateChange() {
|
||||
if (dataChannel.state() == DataChannel.State.OPEN) {
|
||||
dataChannelLatch.countDown();
|
||||
}
|
||||
}
|
||||
};
|
||||
this.dataChannel.registerObserver(stateChangeObserver);
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
this.pipedInputStreamLatch.await();
|
||||
this.dataChannelLatch.await();
|
||||
final var buffer = new byte[4096];
|
||||
while (isSending.get()) {
|
||||
final long bufferedAmount = dataChannel.bufferedAmount();
|
||||
if (bufferedAmount > MAX_SENT_BUFFER) {
|
||||
Thread.sleep(50);
|
||||
continue;
|
||||
}
|
||||
final int count = this.inputStream.read(buffer);
|
||||
if (count < 0) {
|
||||
Log.d(Config.LOGTAG, "DataChannelWriter reached EOF");
|
||||
return;
|
||||
}
|
||||
dataChannel.send(
|
||||
new DataChannel.Buffer(ByteBuffer.wrap(buffer, 0, count), true));
|
||||
}
|
||||
} catch (final InterruptedException | InterruptedIOException e) {
|
||||
if (isSending.get()) {
|
||||
Log.w(Config.LOGTAG, "DataChannelWriter got interrupted while sending", e);
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.d(Config.LOGTAG, "DataChannelWriter terminated", e);
|
||||
} finally {
|
||||
Closeables.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
this.isSending.set(false);
|
||||
terminate(this.dataChannel);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class StateChangeObserver implements DataChannel.Observer {
|
||||
|
||||
@Override
|
||||
public void onBufferedAmountChange(final long change) {}
|
||||
|
||||
@Override
|
||||
public void onMessage(final DataChannel.Buffer buffer) {}
|
||||
}
|
||||
|
||||
private abstract static class OnMessageObserver implements DataChannel.Observer {
|
||||
|
||||
@Override
|
||||
public void onBufferedAmountChange(long l) {}
|
||||
|
||||
@Override
|
||||
public void onStateChange() {}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue