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 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 Jid BUG_REPORTS = Jid.of("bugs@conversations.im");
|
||||||
public static final Uri HELP = Uri.parse("https://help.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 OMEMO_PADDING = false;
|
||||||
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
|
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
|
||||||
public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = 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 USE_DIRECT_JINGLE_CANDIDATES = true;
|
||||||
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
||||||
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
|
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.Jid;
|
||||||
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
|
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
|
||||||
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
|
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.OmemoVerification;
|
||||||
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
|
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
|
||||||
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
|
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
|
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.pep.PublishOptions;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
@ -1262,12 +1264,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
if (Config.REQUIRE_RTP_VERIFICATION) {
|
if (Config.REQUIRE_RTP_VERIFICATION) {
|
||||||
requireVerification(session);
|
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();
|
final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
|
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
|
||||||
omemoVerification.setSessionFingerprint(session.getFingerprint());
|
omemoVerification.setSessionFingerprint(session.getFingerprint());
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : rtpContentMap.contents.entrySet()) {
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : rtpContentMap.contents.entrySet()) {
|
||||||
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
|
||||||
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
|
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
|
||||||
try {
|
try {
|
||||||
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
|
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
|
||||||
|
@ -1276,7 +1278,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
descriptionTransportBuilder.put(
|
descriptionTransportBuilder.put(
|
||||||
content.getKey(),
|
content.getKey(),
|
||||||
new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
|
new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Futures.immediateFuture(
|
return Futures.immediateFuture(
|
||||||
|
@ -1296,11 +1298,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ListenableFuture<OmemoVerifiedPayload<RtpContentMap>> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
|
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 OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
|
final ImmutableList.Builder<ListenableFuture<XmppAxolotlSession>> pepVerificationFutures = new ImmutableList.Builder<>();
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
|
||||||
final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
|
final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport = content.getValue();
|
||||||
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
|
final OmemoVerifiedPayload<IceUdpTransportInfo> decryptedTransport;
|
||||||
try {
|
try {
|
||||||
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
|
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
|
||||||
|
@ -1310,7 +1312,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
omemoVerification.setOrEnsureEqual(decryptedTransport);
|
omemoVerification.setOrEnsureEqual(decryptedTransport);
|
||||||
descriptionTransportBuilder.put(
|
descriptionTransportBuilder.put(
|
||||||
content.getKey(),
|
content.getKey(),
|
||||||
new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
|
new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
processPostponed();
|
processPostponed();
|
||||||
|
@ -1376,18 +1378,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
|
public ListenableFuture<XmppAxolotlMessage> prepareKeyTransportMessage(final Conversation conversation) {
|
||||||
executor.execute(new Runnable() {
|
return Futures.submit(()->{
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||||
if (buildHeader(axolotlMessage, conversation)) {
|
if (buildHeader(axolotlMessage, conversation)) {
|
||||||
onMessageCreatedCallback.run(axolotlMessage);
|
return axolotlMessage;
|
||||||
} else {
|
} else {
|
||||||
onMessageCreatedCallback.run(null);
|
throw new IllegalStateException("No session to decrypt to");
|
||||||
}
|
}
|
||||||
}
|
},executor);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
|
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 static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
||||||
private final String[] FEATURES = {
|
private final String[] FEATURES = {
|
||||||
Namespace.JINGLE,
|
Namespace.JINGLE,
|
||||||
|
Namespace.JINGLE_APPS_FILE_TRANSFER,
|
||||||
//Jingle File Transfer
|
|
||||||
FileTransferDescription.Version.FT_3.getNamespace(),
|
|
||||||
FileTransferDescription.Version.FT_4.getNamespace(),
|
|
||||||
FileTransferDescription.Version.FT_5.getNamespace(),
|
|
||||||
Namespace.JINGLE_TRANSPORTS_S5B,
|
Namespace.JINGLE_TRANSPORTS_S5B,
|
||||||
Namespace.JINGLE_TRANSPORTS_IBB,
|
Namespace.JINGLE_TRANSPORTS_IBB,
|
||||||
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
|
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
|
||||||
|
@ -124,6 +120,7 @@ public abstract class AbstractGenerator {
|
||||||
if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
|
if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
|
||||||
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
|
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
|
||||||
features.addAll(Arrays.asList(VOIP_NAMESPACES));
|
features.addAll(Arrays.asList(VOIP_NAMESPACES));
|
||||||
|
features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
|
||||||
}
|
}
|
||||||
if (mXmppConnectionService.broadcastLastActivity()) {
|
if (mXmppConnectionService.broadcastLastActivity()) {
|
||||||
features.add(Namespace.IDLE);
|
features.add(Namespace.IDLE);
|
||||||
|
|
|
@ -403,7 +403,12 @@ public class UnifiedPushBroker {
|
||||||
updateIntent.putExtra("token", target.instance);
|
updateIntent.putExtra("token", target.instance);
|
||||||
updateIntent.putExtra("bytesMessage", payload);
|
updateIntent.putExtra("bytesMessage", payload);
|
||||||
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
|
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);
|
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_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_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_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_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_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_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
|
||||||
public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
|
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 = "urn:xmpp:reporting:1";
|
||||||
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
|
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 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;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Preconditions;
|
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.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Message;
|
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.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
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 abstract class AbstractJingleConnection {
|
||||||
|
|
||||||
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
|
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
|
||||||
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
|
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;
|
final JingleConnectionManager jingleConnectionManager;
|
||||||
protected final XmppConnectionService xmppConnectionService;
|
protected final XmppConnectionService xmppConnectionService;
|
||||||
protected final Id id;
|
protected final Id id;
|
||||||
private final Jid initiator;
|
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.jingleConnectionManager = jingleConnectionManager;
|
||||||
this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
|
this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.initiator = initiator;
|
this.initiator = initiator;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isInitiator() {
|
|
||||||
return initiator.equals(id.account.getJid());
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract void deliverPacket(JinglePacket jinglePacket);
|
|
||||||
|
|
||||||
public Id getId() {
|
public Id getId() {
|
||||||
return id;
|
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();
|
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 static class Id implements OngoingRtpSession {
|
||||||
public final Account account;
|
public final Account account;
|
||||||
|
@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection {
|
||||||
return new Id(
|
return new Id(
|
||||||
message.getConversation().getAccount(),
|
message.getConversation().getAccount(),
|
||||||
message.getCounterpart(),
|
message.getCounterpart(),
|
||||||
JingleConnectionManager.nextRandomId()
|
JingleConnectionManager.nextRandomId());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contact getContact() {
|
public Contact getContact() {
|
||||||
|
@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
Id id = (Id) o;
|
Id id = (Id) o;
|
||||||
return Objects.equal(account.getUuid(), id.account.getUuid()) &&
|
return Objects.equal(account.getUuid(), id.account.getUuid())
|
||||||
Objects.equal(with, id.with) &&
|
&& Objects.equal(with, id.with)
|
||||||
Objects.equal(sessionId, id.sessionId);
|
&& Objects.equal(sessionId, id.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -122,6 +426,17 @@ 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 {
|
public enum State {
|
||||||
NULL, // default value; nothing has been sent or received yet
|
NULL, // default value; nothing has been sent or received yet
|
||||||
|
@ -137,8 +452,10 @@ public abstract class AbstractJingleConnection {
|
||||||
SESSION_ACCEPTED, // equal to 'ACTIVE'
|
SESSION_ACCEPTED, // equal to 'ACTIVE'
|
||||||
TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
|
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_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_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
|
||||||
TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
|
// 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_APPLICATION_FAILURE,
|
||||||
TERMINATED_SECURITY_ERROR
|
TERMINATED_SECURITY_ERROR
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
|
@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
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 {
|
public final class ContentAddition {
|
||||||
|
|
||||||
|
@ -32,12 +36,13 @@ public final class ContentAddition {
|
||||||
Collections2.transform(
|
Collections2.transform(
|
||||||
rtpContentMap.contents.entrySet(),
|
rtpContentMap.contents.entrySet(),
|
||||||
e -> {
|
e -> {
|
||||||
final RtpContentMap.DescriptionTransport dt = e.getValue();
|
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> dt = e.getValue();
|
||||||
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
|
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
.add("direction", direction)
|
.add("direction", direction)
|
||||||
|
@ -77,6 +82,7 @@ public final class ContentAddition {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this)
|
return MoreObjects.toStringHelper(this)
|
||||||
.add("name", name)
|
.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;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import java.net.Inet6Address;
|
import java.net.Inet6Address;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.NetworkInterface;
|
import java.net.NetworkInterface;
|
||||||
|
@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
|
||||||
public class DirectConnectionUtils {
|
public class DirectConnectionUtils {
|
||||||
|
|
||||||
private static List<InetAddress> getLocalAddresses() {
|
public static List<InetAddress> getLocalAddresses() {
|
||||||
final List<InetAddress> addresses = new ArrayList<>();
|
final ImmutableList.Builder<InetAddress> inetAddresses = new ImmutableList.Builder<>();
|
||||||
final Enumeration<NetworkInterface> interfaces;
|
final Enumeration<NetworkInterface> interfaces;
|
||||||
try {
|
try {
|
||||||
interfaces = NetworkInterface.getNetworkInterfaces();
|
interfaces = NetworkInterface.getNetworkInterfaces();
|
||||||
} catch (SocketException e) {
|
} catch (final SocketException e) {
|
||||||
return addresses;
|
return inetAddresses.build();
|
||||||
}
|
}
|
||||||
while (interfaces.hasMoreElements()) {
|
while (interfaces.hasMoreElements()) {
|
||||||
NetworkInterface networkInterface = interfaces.nextElement();
|
NetworkInterface networkInterface = interfaces.nextElement();
|
||||||
|
@ -34,31 +36,15 @@ public class DirectConnectionUtils {
|
||||||
if (inetAddress instanceof Inet6Address) {
|
if (inetAddress instanceof Inet6Address) {
|
||||||
//let's get rid of scope
|
//let's get rid of scope
|
||||||
try {
|
try {
|
||||||
addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
|
||||||
} catch (UnknownHostException e) {
|
} catch (UnknownHostException e) {
|
||||||
//ignored
|
//ignored
|
||||||
}
|
}
|
||||||
} else {
|
} 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.Content;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
|
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.JinglePacket;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
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.IqPacket;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
|
||||||
|
@ -61,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions =
|
private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions =
|
||||||
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
|
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
|
||||||
|
|
||||||
private final HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
|
|
||||||
|
|
||||||
public JingleConnectionManager(XmppConnectionService service) {
|
public JingleConnectionManager(XmppConnectionService service) {
|
||||||
super(service);
|
super(service);
|
||||||
this.toneManager = new ToneManager(service);
|
this.toneManager = new ToneManager(service);
|
||||||
|
@ -90,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
final String descriptionNamespace =
|
final String descriptionNamespace =
|
||||||
content == null ? null : content.getDescriptionNamespace();
|
content == null ? null : content.getDescriptionNamespace();
|
||||||
final AbstractJingleConnection connection;
|
final AbstractJingleConnection connection;
|
||||||
if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
|
if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) {
|
||||||
connection = new JingleFileTransferConnection(this, id, from);
|
connection = new JingleFileTransferConnection(this, id, from);
|
||||||
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
|
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
|
||||||
&& isUsingClearNet(account)) {
|
&& isUsingClearNet(account)) {
|
||||||
|
@ -593,13 +594,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
if (old != null) {
|
if (old != null) {
|
||||||
old.cancel();
|
old.cancel();
|
||||||
}
|
}
|
||||||
final Account account = message.getConversation().getAccount();
|
|
||||||
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
|
|
||||||
final JingleFileTransferConnection connection =
|
final JingleFileTransferConnection connection =
|
||||||
new JingleFileTransferConnection(this, id, account.getJid());
|
new JingleFileTransferConnection(this, message);
|
||||||
mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
|
this.connections.put(connection.getId(), connection);
|
||||||
this.connections.put(id, connection);
|
connection.sendSessionInitialize();
|
||||||
connection.init(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
|
public Optional<OngoingRtpSession> getOngoingRtpConnection(final Contact contact) {
|
||||||
|
@ -658,60 +656,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
return firedUpdates;
|
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) {
|
public void retractSessionProposal(final Account account, final Jid with) {
|
||||||
synchronized (this.rtpSessionProposals) {
|
synchronized (this.rtpSessionProposals) {
|
||||||
RtpSessionProposal matchingProposal = null;
|
RtpSessionProposal matchingProposal = null;
|
||||||
|
@ -810,36 +754,53 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deliverIbbPacket(Account account, IqPacket packet) {
|
public void deliverIbbPacket(final Account account, final IqPacket packet) {
|
||||||
final String sid;
|
final String sid;
|
||||||
final Element payload;
|
final Element payload;
|
||||||
|
final InbandBytestreamsTransport.PacketType packetType;
|
||||||
if (packet.hasChild("open", Namespace.IBB)) {
|
if (packet.hasChild("open", Namespace.IBB)) {
|
||||||
|
packetType = InbandBytestreamsTransport.PacketType.OPEN;
|
||||||
payload = packet.findChild("open", Namespace.IBB);
|
payload = packet.findChild("open", Namespace.IBB);
|
||||||
sid = payload.getAttribute("sid");
|
sid = payload.getAttribute("sid");
|
||||||
} else if (packet.hasChild("data", Namespace.IBB)) {
|
} else if (packet.hasChild("data", Namespace.IBB)) {
|
||||||
|
packetType = InbandBytestreamsTransport.PacketType.DATA;
|
||||||
payload = packet.findChild("data", Namespace.IBB);
|
payload = packet.findChild("data", Namespace.IBB);
|
||||||
sid = payload.getAttribute("sid");
|
sid = payload.getAttribute("sid");
|
||||||
} else if (packet.hasChild("close", Namespace.IBB)) {
|
} else if (packet.hasChild("close", Namespace.IBB)) {
|
||||||
|
packetType = InbandBytestreamsTransport.PacketType.CLOSE;
|
||||||
payload = packet.findChild("close", Namespace.IBB);
|
payload = packet.findChild("close", Namespace.IBB);
|
||||||
sid = payload.getAttribute("sid");
|
sid = payload.getAttribute("sid");
|
||||||
} else {
|
} else {
|
||||||
|
packetType = null;
|
||||||
payload = null;
|
payload = null;
|
||||||
sid = null;
|
sid = null;
|
||||||
}
|
}
|
||||||
if (sid != null) {
|
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()) {
|
for (final AbstractJingleConnection connection : this.connections.values()) {
|
||||||
if (connection instanceof JingleFileTransferConnection fileTransfer) {
|
if (connection instanceof JingleFileTransferConnection fileTransfer) {
|
||||||
final JingleTransport transport = fileTransfer.getTransport();
|
final Transport transport = fileTransfer.getTransport();
|
||||||
if (transport instanceof JingleInBandTransport inBandTransport) {
|
if (transport instanceof InbandBytestreamsTransport inBandTransport) {
|
||||||
if (inBandTransport.matches(account, sid)) {
|
if (sid.equals(inBandTransport.getStreamId())) {
|
||||||
inBandTransport.deliverPayload(packet, payload);
|
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;
|
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()
|
account.getXmppConnection()
|
||||||
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
|
.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.base.Throwables;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import com.google.common.collect.ImmutableMultimap;
|
import com.google.common.collect.ImmutableMultimap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
import com.google.common.collect.Iterables;
|
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.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
|
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
|
||||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
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.Conversation;
|
||||||
import eu.siacs.conversations.entities.Conversational;
|
import eu.siacs.conversations.entities.Conversational;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.Presence;
|
|
||||||
import eu.siacs.conversations.entities.RtpSessionStatus;
|
import eu.siacs.conversations.entities.RtpSessionStatus;
|
||||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
|
||||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
import eu.siacs.conversations.utils.IP;
|
import eu.siacs.conversations.utils.IP;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
@ -78,96 +73,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
|
State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
|
||||||
private static final long BUSY_TIME_OUT = 30;
|
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 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<>();
|
pendingIceCandidates = new LinkedList<>();
|
||||||
private final OmemoVerification omemoVerification = new OmemoVerification();
|
private final OmemoVerification omemoVerification = new OmemoVerification();
|
||||||
private final Message message;
|
private final Message message;
|
||||||
private State state = State.NULL;
|
|
||||||
private Set<Media> proposedMedia;
|
private Set<Media> proposedMedia;
|
||||||
private RtpContentMap initiatorRtpContentMap;
|
private RtpContentMap initiatorRtpContentMap;
|
||||||
private RtpContentMap responderRtpContentMap;
|
private RtpContentMap responderRtpContentMap;
|
||||||
|
@ -192,18 +104,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
id.sessionId);
|
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
|
@Override
|
||||||
synchronized void deliverPacket(final JinglePacket jinglePacket) {
|
synchronized void deliverPacket(final JinglePacket jinglePacket) {
|
||||||
switch (jinglePacket.getAction()) {
|
switch (jinglePacket.getAction()) {
|
||||||
|
@ -233,7 +133,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
webRTCWrapper.close();
|
webRTCWrapper.close();
|
||||||
if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
|
if (isResponder() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
|
||||||
xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
|
xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
|
||||||
}
|
}
|
||||||
if (isInState(
|
if (isInState(
|
||||||
|
@ -322,7 +222,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
|
|
||||||
private void receiveTransportInfo(
|
private void receiveTransportInfo(
|
||||||
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
|
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();
|
contentMap.contents.entrySet();
|
||||||
final RtpContentMap remote = getRemoteContentMap();
|
final RtpContentMap remote = getRemoteContentMap();
|
||||||
final Set<String> remoteContentIds =
|
final Set<String> remoteContentIds =
|
||||||
|
@ -522,7 +422,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
|
|
||||||
setRemoteContentMap(modifiedContentMap);
|
setRemoteContentMap(modifiedContentMap);
|
||||||
|
|
||||||
final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator());
|
final SessionDescription answer = SessionDescription.of(modifiedContentMap, isResponder());
|
||||||
|
|
||||||
final org.webrtc.SessionDescription sdp =
|
final org.webrtc.SessionDescription sdp =
|
||||||
new org.webrtc.SessionDescription(
|
new org.webrtc.SessionDescription(
|
||||||
|
@ -596,7 +496,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
final SessionDescription offer;
|
final SessionDescription offer;
|
||||||
try {
|
try {
|
||||||
offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator());
|
offer = SessionDescription.of(modifiedRemoteContentMap, isResponder());
|
||||||
} catch (final IllegalArgumentException | NullPointerException e) {
|
} catch (final IllegalArgumentException | NullPointerException e) {
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
|
@ -815,7 +715,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
final RtpContentMap nextRemote =
|
final RtpContentMap nextRemote =
|
||||||
currentRemote.addContent(
|
currentRemote.addContent(
|
||||||
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
|
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
|
||||||
return SessionDescription.of(nextRemote, !isInitiator());
|
return SessionDescription.of(nextRemote, isResponder());
|
||||||
}
|
}
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Unexpected rollback condition. Senders were not uniformly none");
|
"Unexpected rollback condition. Senders were not uniformly none");
|
||||||
|
@ -881,7 +781,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
|
|
||||||
final SessionDescription offer;
|
final SessionDescription offer;
|
||||||
try {
|
try {
|
||||||
offer = SessionDescription.of(modifiedContentMap, !isInitiator());
|
offer = SessionDescription.of(modifiedContentMap, isResponder());
|
||||||
} catch (final IllegalArgumentException | NullPointerException e) {
|
} catch (final IllegalArgumentException | NullPointerException e) {
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
|
@ -1066,7 +966,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
final boolean isOffer)
|
final boolean isOffer)
|
||||||
throws ExecutionException, InterruptedException {
|
throws ExecutionException, InterruptedException {
|
||||||
final SessionDescription sessionDescription =
|
final SessionDescription sessionDescription =
|
||||||
SessionDescription.of(restartContentMap, !isInitiator());
|
SessionDescription.of(restartContentMap, isResponder());
|
||||||
final org.webrtc.SessionDescription.Type type =
|
final org.webrtc.SessionDescription.Type type =
|
||||||
isOffer
|
isOffer
|
||||||
? org.webrtc.SessionDescription.Type.OFFER
|
? org.webrtc.SessionDescription.Type.OFFER
|
||||||
|
@ -1095,14 +995,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processCandidates(
|
private void processCandidates(
|
||||||
final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
|
final Set<Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>>> contents) {
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content : contents) {
|
||||||
processCandidate(content);
|
processCandidate(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processCandidate(
|
private void processCandidate(
|
||||||
final Map.Entry<String, RtpContentMap.DescriptionTransport> content) {
|
final Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> content) {
|
||||||
final RtpContentMap rtpContentMap = getRemoteContentMap();
|
final RtpContentMap rtpContentMap = getRemoteContentMap();
|
||||||
final List<String> indices = toIdentificationTags(rtpContentMap);
|
final List<String> indices = toIdentificationTags(rtpContentMap);
|
||||||
final String sdpMid = content.getKey(); // aka content name
|
final String sdpMid = content.getKey(); // aka content name
|
||||||
|
@ -1204,21 +1104,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
|
|
||||||
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
|
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
|
||||||
if (isInitiator()) {
|
if (isInitiator()) {
|
||||||
Log.d(
|
receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
|
final ListenableFuture<RtpContentMap> future = receiveRtpContentMap(jinglePacket, false);
|
||||||
|
@ -1300,13 +1186,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private void receiveSessionAccept(final JinglePacket jinglePacket) {
|
private void receiveSessionAccept(final JinglePacket jinglePacket) {
|
||||||
if (!isInitiator()) {
|
if (isResponder()) {
|
||||||
Log.d(
|
receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
|
||||||
Config.LOGTAG,
|
|
||||||
String.format(
|
|
||||||
"%s: received session-accept even though we were responding",
|
|
||||||
id.account.getJid().asBareJid()));
|
|
||||||
terminateWithOutOfOrder(jinglePacket);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ListenableFuture<RtpContentMap> future =
|
final ListenableFuture<RtpContentMap> future =
|
||||||
|
@ -1491,7 +1372,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addIceCandidatesFromBlackLog() {
|
private void addIceCandidatesFromBlackLog() {
|
||||||
Map.Entry<String, RtpContentMap.DescriptionTransport> foo;
|
Map.Entry<String, DescriptionTransport<RtpDescription,IceUdpTransportInfo>> foo;
|
||||||
while ((foo = this.pendingIceCandidates.poll()) != null) {
|
while ((foo = this.pendingIceCandidates.poll()) != null) {
|
||||||
processCandidate(foo);
|
processCandidate(foo);
|
||||||
Log.d(
|
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);
|
sendSessionTerminate(reason, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendSessionTerminate(final Reason reason, final String text) {
|
|
||||||
final State previous = this.state;
|
protected void sendSessionTerminate(final Reason reason, final String text) {
|
||||||
final State target = reasonToState(reason);
|
sendSessionTerminate(reason,text, this::writeLogMessage);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sendTransportInfo(
|
private void sendTransportInfo(
|
||||||
final String contentName, IceUdpTransportInfo.Candidate candidate) {
|
final String contentName, IceUdpTransportInfo.Candidate candidate) {
|
||||||
final RtpContentMap transportInfo;
|
final RtpContentMap transportInfo;
|
||||||
|
@ -2099,110 +1972,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
send(jinglePacket);
|
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() {
|
public RtpEndUserState getEndUserState() {
|
||||||
switch (this.state) {
|
switch (this.state) {
|
||||||
case NULL, PROPOSED, SESSION_INITIALIZED -> {
|
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");
|
+ ": received endCall() when session has already been terminated. nothing to do");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isInState(State.PROPOSED) && !isInitiator()) {
|
if (isInState(State.PROPOSED) && isResponder()) {
|
||||||
rejectCallFromProposed();
|
rejectCallFromProposed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2527,22 +2296,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
sendSessionAccept();
|
sendSessionAccept();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized boolean isInState(State... state) {
|
|
||||||
return Arrays.asList(state).contains(this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean transition(final State target) {
|
@Override
|
||||||
return transition(target, null);
|
protected synchronized boolean transition(final State target, final Runnable runnable) {
|
||||||
}
|
if (super.transition(target, runnable)) {
|
||||||
|
|
||||||
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);
|
|
||||||
updateEndUserState();
|
updateEndUserState();
|
||||||
updateOngoingCallNotification();
|
updateOngoingCallNotification();
|
||||||
return true;
|
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
|
@Override
|
||||||
public void onIceCandidate(final IceCandidate iceCandidate) {
|
public void onIceCandidate(final IceCandidate iceCandidate) {
|
||||||
final RtpContentMap rtpContentMap =
|
final RtpContentMap rtpContentMap =
|
||||||
|
@ -2893,98 +2643,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
id.account,
|
id.account,
|
||||||
request,
|
request,
|
||||||
(account, response) -> {
|
(account, response) -> {
|
||||||
ImmutableList.Builder<PeerConnection.IceServer> listBuilder =
|
final var iceServers = IceServers.parse(response);
|
||||||
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();
|
|
||||||
if (iceServers.size() == 0) {
|
if (iceServers.size() == 0) {
|
||||||
Log.w(
|
Log.w(
|
||||||
Config.LOGTAG,
|
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()) {
|
if (isTerminated()) {
|
||||||
this.cancelRingingTimeout();
|
this.cancelRingingTimeout();
|
||||||
this.webRTCWrapper.verifyClosed();
|
this.webRTCWrapper.verifyClosed();
|
||||||
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
|
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
|
||||||
this.jingleConnectionManager.finishConnectionOrThrow(this);
|
super.finish();
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
String.format("Unable to call finish from %s", this.state));
|
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() {
|
public Optional<VideoTrack> getLocalVideoTrack() {
|
||||||
return webRTCWrapper.getLocalVideoTrack();
|
return webRTCWrapper.getLocalVideoTrack();
|
||||||
}
|
}
|
||||||
|
@ -3091,17 +2748,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return remoteHasFeature(Namespace.SDP_OFFER_ANSWER);
|
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 {
|
private interface OnIceServersDiscovered {
|
||||||
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
|
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;
|
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;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -8,9 +9,9 @@ public class MediaBuilder {
|
||||||
private String media;
|
private String media;
|
||||||
private int port;
|
private int port;
|
||||||
private String protocol;
|
private String protocol;
|
||||||
private List<Integer> formats;
|
private String format;
|
||||||
private String connectionData;
|
private String connectionData;
|
||||||
private ArrayListMultimap<String,String> attributes;
|
private Multimap<String, String> attributes;
|
||||||
|
|
||||||
public MediaBuilder setMedia(String media) {
|
public MediaBuilder setMedia(String media) {
|
||||||
this.media = media;
|
this.media = media;
|
||||||
|
@ -27,8 +28,13 @@ public class MediaBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaBuilder setFormats(List<Integer> formats) {
|
public MediaBuilder setFormats(final List<Integer> formats) {
|
||||||
this.formats = formats;
|
this.format = Joiner.on(' ').join(formats);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaBuilder setFormat(final String format) {
|
||||||
|
this.format = format;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,12 +43,13 @@ public class MediaBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
|
public MediaBuilder setAttributes(Multimap<String, String> attributes) {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SessionDescription.Media createMedia() {
|
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 java.util.Map;
|
||||||
|
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
|
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.OmemoVerifiedIceUdpTransportInfo;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||||
|
|
||||||
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
|
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);
|
super(group, contents);
|
||||||
for(final DescriptionTransport descriptionTransport : contents.values()) {
|
for(final DescriptionTransport<RtpDescription,IceUdpTransportInfo> descriptionTransport : contents.values()) {
|
||||||
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
||||||
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
|
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
|
||||||
continue;
|
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.Predicates;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.ImmutableMultimap;
|
import com.google.common.collect.ImmutableMultimap;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
@ -31,19 +30,17 @@ import java.util.Set;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public class RtpContentMap {
|
public class RtpContentMap extends AbstractContentMap<RtpDescription, IceUdpTransportInfo> {
|
||||||
|
|
||||||
public final Group group;
|
public RtpContentMap(
|
||||||
public final Map<String, DescriptionTransport> contents;
|
Group group,
|
||||||
|
Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
|
||||||
public RtpContentMap(Group group, Map<String, DescriptionTransport> contents) {
|
super(group, contents);
|
||||||
this.group = group;
|
|
||||||
this.contents = contents;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RtpContentMap of(final JinglePacket jinglePacket) {
|
public static RtpContentMap of(final JinglePacket jinglePacket) {
|
||||||
final Map<String, DescriptionTransport> contents =
|
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents =
|
||||||
DescriptionTransport.of(jinglePacket.getJingleContents());
|
of(jinglePacket.getJingleContents());
|
||||||
if (isOmemoVerified(contents)) {
|
if (isOmemoVerified(contents)) {
|
||||||
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
|
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,12 +48,15 @@ public class RtpContentMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isOmemoVerified(Map<String, DescriptionTransport> contents) {
|
private static boolean isOmemoVerified(
|
||||||
final Collection<DescriptionTransport> values = contents.values();
|
Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> contents) {
|
||||||
|
final Collection<DescriptionTransport<RtpDescription, IceUdpTransportInfo>> values =
|
||||||
|
contents.values();
|
||||||
if (values.size() == 0) {
|
if (values.size() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (final DescriptionTransport descriptionTransport : values) {
|
for (final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport :
|
||||||
|
values) {
|
||||||
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -67,13 +67,13 @@ public class RtpContentMap {
|
||||||
|
|
||||||
public static RtpContentMap of(
|
public static RtpContentMap of(
|
||||||
final SessionDescription sessionDescription, final boolean isInitiator) {
|
final SessionDescription sessionDescription, final boolean isInitiator) {
|
||||||
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
final ImmutableMap.Builder<
|
||||||
new ImmutableMap.Builder<>();
|
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
|
contentMapBuilder = new ImmutableMap.Builder<>();
|
||||||
for (SessionDescription.Media media : sessionDescription.media) {
|
for (SessionDescription.Media media : sessionDescription.media) {
|
||||||
final String id = Iterables.getFirst(media.attributes.get("mid"), null);
|
final String id = Iterables.getFirst(media.attributes.get("mid"), null);
|
||||||
Preconditions.checkNotNull(id, "media has no mid");
|
Preconditions.checkNotNull(id, "media has no mid");
|
||||||
contentMapBuilder.put(
|
contentMapBuilder.put(id, of(sessionDescription, isInitiator, media));
|
||||||
id, DescriptionTransport.of(sessionDescription, isInitiator, media));
|
|
||||||
}
|
}
|
||||||
final String groupAttribute =
|
final String groupAttribute =
|
||||||
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
|
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() {
|
void requireDTLSFingerprint() {
|
||||||
requireDTLSFingerprint(false);
|
requireDTLSFingerprint(false);
|
||||||
}
|
}
|
||||||
|
@ -122,7 +102,8 @@ public class RtpContentMap {
|
||||||
if (this.contents.size() == 0) {
|
if (this.contents.size() == 0) {
|
||||||
throw new IllegalStateException("No contents available");
|
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 transport = entry.getValue().transport;
|
||||||
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
|
||||||
if (fingerprint == null
|
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(
|
RtpContentMap transportInfo(
|
||||||
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
|
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 =
|
final IceUdpTransportInfo transportInfo =
|
||||||
descriptionTransport == null ? null : descriptionTransport.transport;
|
descriptionTransport == null ? null : descriptionTransport.transport;
|
||||||
if (transportInfo == null) {
|
if (transportInfo == null) {
|
||||||
|
@ -183,7 +143,7 @@ public class RtpContentMap {
|
||||||
null,
|
null,
|
||||||
ImmutableMap.of(
|
ImmutableMap.of(
|
||||||
contentName,
|
contentName,
|
||||||
new DescriptionTransport(
|
new DescriptionTransport<>(
|
||||||
descriptionTransport.senders, null, newTransportInfo)));
|
descriptionTransport.senders, null, newTransportInfo)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,21 +153,24 @@ public class RtpContentMap {
|
||||||
Maps.transformValues(
|
Maps.transformValues(
|
||||||
contents,
|
contents,
|
||||||
dt ->
|
dt ->
|
||||||
new DescriptionTransport(
|
new DescriptionTransport<>(
|
||||||
dt.senders, null, dt.transport.cloneWrapper())));
|
dt.senders, null, dt.transport.cloneWrapper())));
|
||||||
}
|
}
|
||||||
|
|
||||||
RtpContentMap withCandidates(
|
RtpContentMap withCandidates(
|
||||||
ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) {
|
ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates) {
|
||||||
final ImmutableMap.Builder<String, DescriptionTransport> contentBuilder =
|
final ImmutableMap.Builder<
|
||||||
new ImmutableMap.Builder<>();
|
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
for (final Map.Entry<String, DescriptionTransport> entry : this.contents.entrySet()) {
|
contentBuilder = new ImmutableMap.Builder<>();
|
||||||
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
|
entry : this.contents.entrySet()) {
|
||||||
final String name = entry.getKey();
|
final String name = entry.getKey();
|
||||||
final DescriptionTransport descriptionTransport = entry.getValue();
|
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
|
||||||
|
entry.getValue();
|
||||||
final var transport = descriptionTransport.transport;
|
final var transport = descriptionTransport.transport;
|
||||||
contentBuilder.put(
|
contentBuilder.put(
|
||||||
name,
|
name,
|
||||||
new DescriptionTransport(
|
new DescriptionTransport<>(
|
||||||
descriptionTransport.senders,
|
descriptionTransport.senders,
|
||||||
descriptionTransport.description,
|
descriptionTransport.description,
|
||||||
transport.withCandidates(candidates.get(name))));
|
transport.withCandidates(candidates.get(name))));
|
||||||
|
@ -247,7 +210,7 @@ public class RtpContentMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
|
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
|
||||||
final DescriptionTransport descriptionTransport = this.contents.get(contentName);
|
final var descriptionTransport = this.contents.get(contentName);
|
||||||
if (descriptionTransport == null) {
|
if (descriptionTransport == null) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
String.format(
|
String.format(
|
||||||
|
@ -287,7 +250,7 @@ public class RtpContentMap {
|
||||||
|
|
||||||
public boolean emptyCandidates() {
|
public boolean emptyCandidates() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (DescriptionTransport descriptionTransport : contents.values()) {
|
for (final var descriptionTransport : contents.values()) {
|
||||||
count += descriptionTransport.transport.getCandidates().size();
|
count += descriptionTransport.transport.getCandidates().size();
|
||||||
}
|
}
|
||||||
return count == 0;
|
return count == 0;
|
||||||
|
@ -300,17 +263,19 @@ public class RtpContentMap {
|
||||||
|
|
||||||
public RtpContentMap modifiedCredentials(
|
public RtpContentMap modifiedCredentials(
|
||||||
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
|
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
|
||||||
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
final ImmutableMap.Builder<
|
||||||
new ImmutableMap.Builder<>();
|
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
|
contentMapBuilder = new ImmutableMap.Builder<>();
|
||||||
final DescriptionTransport descriptionTransport = content.getValue();
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
|
content : contents.entrySet()) {
|
||||||
|
final var descriptionTransport = content.getValue();
|
||||||
final RtpDescription rtpDescription = descriptionTransport.description;
|
final RtpDescription rtpDescription = descriptionTransport.description;
|
||||||
final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
|
final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
|
||||||
final IceUdpTransportInfo modifiedTransportInfo =
|
final IceUdpTransportInfo modifiedTransportInfo =
|
||||||
transportInfo.modifyCredentials(credentials, setup);
|
transportInfo.modifyCredentials(credentials, setup);
|
||||||
contentMapBuilder.put(
|
contentMapBuilder.put(
|
||||||
content.getKey(),
|
content.getKey(),
|
||||||
new DescriptionTransport(
|
new DescriptionTransport<>(
|
||||||
descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
|
descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
|
||||||
}
|
}
|
||||||
return new RtpContentMap(this.group, contentMapBuilder.build());
|
return new RtpContentMap(this.group, contentMapBuilder.build());
|
||||||
|
@ -321,16 +286,18 @@ public class RtpContentMap {
|
||||||
this.group,
|
this.group,
|
||||||
Maps.transformValues(
|
Maps.transformValues(
|
||||||
contents,
|
contents,
|
||||||
dt -> new DescriptionTransport(senders, dt.description, dt.transport)));
|
dt -> new DescriptionTransport<>(senders, dt.description, dt.transport)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public RtpContentMap modifiedSendersChecked(
|
public RtpContentMap modifiedSendersChecked(
|
||||||
final boolean isInitiator, final Map<String, Content.Senders> modification) {
|
final boolean isInitiator, final Map<String, Content.Senders> modification) {
|
||||||
final ImmutableMap.Builder<String, DescriptionTransport> contentMapBuilder =
|
final ImmutableMap.Builder<
|
||||||
new ImmutableMap.Builder<>();
|
String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
for (final Map.Entry<String, DescriptionTransport> content : contents.entrySet()) {
|
contentMapBuilder = new ImmutableMap.Builder<>();
|
||||||
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
|
content : contents.entrySet()) {
|
||||||
final String id = content.getKey();
|
final String id = content.getKey();
|
||||||
final DescriptionTransport descriptionTransport = content.getValue();
|
final var descriptionTransport = content.getValue();
|
||||||
final Content.Senders currentSenders = descriptionTransport.senders;
|
final Content.Senders currentSenders = descriptionTransport.senders;
|
||||||
final Content.Senders targetSenders = modification.get(id);
|
final Content.Senders targetSenders = modification.get(id);
|
||||||
if (targetSenders == null || currentSenders == targetSenders) {
|
if (targetSenders == null || currentSenders == targetSenders) {
|
||||||
|
@ -339,7 +306,7 @@ public class RtpContentMap {
|
||||||
checkSenderModification(isInitiator, currentSenders, targetSenders);
|
checkSenderModification(isInitiator, currentSenders, targetSenders);
|
||||||
contentMapBuilder.put(
|
contentMapBuilder.put(
|
||||||
id,
|
id,
|
||||||
new DescriptionTransport(
|
new DescriptionTransport<>(
|
||||||
targetSenders,
|
targetSenders,
|
||||||
descriptionTransport.description,
|
descriptionTransport.description,
|
||||||
descriptionTransport.transport));
|
descriptionTransport.transport));
|
||||||
|
@ -386,7 +353,7 @@ public class RtpContentMap {
|
||||||
Maps.transformValues(
|
Maps.transformValues(
|
||||||
this.contents,
|
this.contents,
|
||||||
dt ->
|
dt ->
|
||||||
new DescriptionTransport(
|
new DescriptionTransport<>(
|
||||||
dt.senders,
|
dt.senders,
|
||||||
RtpDescription.stub(dt.description.getMedia()),
|
RtpDescription.stub(dt.description.getMedia()),
|
||||||
IceUdpTransportInfo.STUB)));
|
IceUdpTransportInfo.STUB)));
|
||||||
|
@ -415,8 +382,10 @@ public class RtpContentMap {
|
||||||
|
|
||||||
public RtpContentMap addContent(
|
public RtpContentMap addContent(
|
||||||
final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
|
final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
|
||||||
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
|
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
|
||||||
final Map<String, DescriptionTransport> combinedFixedTransport =
|
merge(contents, modification.contents);
|
||||||
|
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
|
combinedFixedTransport =
|
||||||
Maps.transformValues(
|
Maps.transformValues(
|
||||||
combined,
|
combined,
|
||||||
dt -> {
|
dt -> {
|
||||||
|
@ -424,7 +393,8 @@ public class RtpContentMap {
|
||||||
if (dt.transport.isStub()) {
|
if (dt.transport.isStub()) {
|
||||||
final IceUdpTransportInfo.Credentials credentials =
|
final IceUdpTransportInfo.Credentials credentials =
|
||||||
getDistinctCredentials();
|
getDistinctCredentials();
|
||||||
final Collection<String> iceOptions = getCombinedIceOptions();
|
final Collection<String> iceOptions =
|
||||||
|
getCombinedIceOptions();
|
||||||
final DTLS dtls = getDistinctDtls();
|
final DTLS dtls = getDistinctDtls();
|
||||||
iceUdpTransportInfo =
|
iceUdpTransportInfo =
|
||||||
IceUdpTransportInfo.of(
|
IceUdpTransportInfo.of(
|
||||||
|
@ -447,35 +417,24 @@ public class RtpContentMap {
|
||||||
fp.getHash(),
|
fp.getHash(),
|
||||||
fp.getContent());
|
fp.getContent());
|
||||||
}
|
}
|
||||||
return new DescriptionTransport(
|
return new DescriptionTransport<>(
|
||||||
dt.senders, dt.description, iceUdpTransportInfo);
|
dt.senders, dt.description, iceUdpTransportInfo);
|
||||||
});
|
});
|
||||||
return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
|
return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, DescriptionTransport> merge(
|
private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> merge(
|
||||||
final Map<String, DescriptionTransport> a, final Map<String, DescriptionTransport> b) {
|
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> a,
|
||||||
final Map<String, DescriptionTransport> combined = new LinkedHashMap<>();
|
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> b) {
|
||||||
|
final Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> combined =
|
||||||
|
new LinkedHashMap<>();
|
||||||
combined.putAll(a);
|
combined.putAll(a);
|
||||||
combined.putAll(b);
|
combined.putAll(b);
|
||||||
return ImmutableMap.copyOf(combined);
|
return ImmutableMap.copyOf(combined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DescriptionTransport {
|
public static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
|
||||||
public final Content.Senders senders;
|
final Content content) {
|
||||||
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 of(final Content content) {
|
|
||||||
final GenericDescription description = content.getDescription();
|
final GenericDescription description = content.getDescription();
|
||||||
final GenericTransportInfo transportInfo = content.getTransport();
|
final GenericTransportInfo transportInfo = content.getTransport();
|
||||||
final Content.Senders senders = content.getSenders();
|
final Content.Senders senders = content.getSenders();
|
||||||
|
@ -486,49 +445,33 @@ public class RtpContentMap {
|
||||||
} else if (description instanceof RtpDescription) {
|
} else if (description instanceof RtpDescription) {
|
||||||
rtpDescription = (RtpDescription) description;
|
rtpDescription = (RtpDescription) description;
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedApplicationException(
|
throw new UnsupportedApplicationException("Content does not contain rtp description");
|
||||||
"Content does not contain rtp description");
|
|
||||||
}
|
}
|
||||||
if (transportInfo instanceof IceUdpTransportInfo) {
|
if (transportInfo instanceof IceUdpTransportInfo) {
|
||||||
iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
|
iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedTransportException(
|
throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
|
||||||
"Content does not contain ICE-UDP transport");
|
|
||||||
}
|
}
|
||||||
return new DescriptionTransport(
|
return new DescriptionTransport<>(
|
||||||
senders,
|
senders,
|
||||||
rtpDescription,
|
rtpDescription,
|
||||||
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
|
OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DescriptionTransport of(
|
private static DescriptionTransport<RtpDescription, IceUdpTransportInfo> of(
|
||||||
final SessionDescription sessionDescription,
|
final SessionDescription sessionDescription,
|
||||||
final boolean isInitiator,
|
final boolean isInitiator,
|
||||||
final SessionDescription.Media media) {
|
final SessionDescription.Media media) {
|
||||||
final Content.Senders senders = Content.Senders.of(media, isInitiator);
|
final Content.Senders senders = Content.Senders.of(media, isInitiator);
|
||||||
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
|
final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
|
||||||
final IceUdpTransportInfo transportInfo =
|
final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
|
||||||
IceUdpTransportInfo.of(sessionDescription, media);
|
return new DescriptionTransport<>(senders, rtpDescription, transportInfo);
|
||||||
return new DescriptionTransport(senders, rtpDescription, transportInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, DescriptionTransport> of(final Map<String, Content> contents) {
|
private static Map<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>> of(
|
||||||
|
final Map<String, Content> contents) {
|
||||||
return ImmutableMap.copyOf(
|
return ImmutableMap.copyOf(
|
||||||
Maps.transformValues(
|
Maps.transformValues(contents, content -> content == null ? null : of(content)));
|
||||||
contents, content -> content == null ? null : of(content)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UnsupportedApplicationException extends IllegalArgumentException {
|
|
||||||
UnsupportedApplicationException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UnsupportedTransportException extends IllegalArgumentException {
|
|
||||||
UnsupportedTransportException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Diff {
|
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.base.Strings;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.ImmutableList;
|
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.Config;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
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.Group;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -28,6 +33,8 @@ public class SessionDescription {
|
||||||
public static final String LINE_DIVIDER = "\r\n";
|
public static final String LINE_DIVIDER = "\r\n";
|
||||||
private static final String HARDCODED_MEDIA_PROTOCOL =
|
private static final String HARDCODED_MEDIA_PROTOCOL =
|
||||||
"UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint
|
"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 int HARDCODED_MEDIA_PORT = 9;
|
||||||
private static final Collection<String> HARDCODED_ICE_OPTIONS =
|
private static final Collection<String> HARDCODED_ICE_OPTIONS =
|
||||||
Collections.singleton("trickle");
|
Collections.singleton("trickle");
|
||||||
|
@ -52,9 +59,8 @@ public class SessionDescription {
|
||||||
this.media = media;
|
this.media = media;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void appendAttributes(
|
private static void appendAttributes(StringBuilder s, Multimap<String, String> attributes) {
|
||||||
StringBuilder s, ArrayListMultimap<String, String> attributes) {
|
for (final Map.Entry<String, String> attribute : attributes.entries()) {
|
||||||
for (Map.Entry<String, String> attribute : attributes.entries()) {
|
|
||||||
final String key = attribute.getKey();
|
final String key = attribute.getKey();
|
||||||
final String value = attribute.getValue();
|
final String value = attribute.getValue();
|
||||||
s.append("a=").append(key);
|
s.append("a=").append(key);
|
||||||
|
@ -79,24 +85,20 @@ public class SessionDescription {
|
||||||
final char key = pair[0].charAt(0);
|
final char key = pair[0].charAt(0);
|
||||||
final String value = pair[1];
|
final String value = pair[1];
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'v':
|
case 'v' -> sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
|
||||||
sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
|
case 'c' -> {
|
||||||
break;
|
|
||||||
case 'c':
|
|
||||||
if (currentMediaBuilder != null) {
|
if (currentMediaBuilder != null) {
|
||||||
currentMediaBuilder.setConnectionData(value);
|
currentMediaBuilder.setConnectionData(value);
|
||||||
} else {
|
} else {
|
||||||
sessionDescriptionBuilder.setConnectionData(value);
|
sessionDescriptionBuilder.setConnectionData(value);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case 's':
|
case 's' -> sessionDescriptionBuilder.setName(value);
|
||||||
sessionDescriptionBuilder.setName(value);
|
case 'a' -> {
|
||||||
break;
|
|
||||||
case 'a':
|
|
||||||
final Pair<String, String> attribute = parseAttribute(value);
|
final Pair<String, String> attribute = parseAttribute(value);
|
||||||
attributeMap.put(attribute.first, attribute.second);
|
attributeMap.put(attribute.first, attribute.second);
|
||||||
break;
|
}
|
||||||
case 'm':
|
case 'm' -> {
|
||||||
if (currentMediaBuilder == null) {
|
if (currentMediaBuilder == null) {
|
||||||
sessionDescriptionBuilder.setAttributes(attributeMap);
|
sessionDescriptionBuilder.setAttributes(attributeMap);
|
||||||
} else {
|
} else {
|
||||||
|
@ -118,7 +120,7 @@ public class SessionDescription {
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, "skipping media line " + line);
|
Log.d(Config.LOGTAG, "skipping media line " + line);
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentMediaBuilder != null) {
|
if (currentMediaBuilder != null) {
|
||||||
|
@ -131,6 +133,56 @@ public class SessionDescription {
|
||||||
return sessionDescriptionBuilder.createSessionDescription();
|
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(
|
public static SessionDescription of(
|
||||||
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
|
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
|
||||||
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
|
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
|
||||||
|
@ -140,58 +192,27 @@ public class SessionDescription {
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
final String semantics = group.getSemantics();
|
final String semantics = group.getSemantics();
|
||||||
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
|
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
|
||||||
attributeMap.put(
|
final var idTags = group.getIdentificationTags();
|
||||||
"group",
|
for (final String content : idTags) {
|
||||||
group.getSemantics()
|
checkNoWhitespace(content, "group content names must not contain any whitespace");
|
||||||
+ " "
|
}
|
||||||
+ Joiner.on(' ').join(group.getIdentificationTags()));
|
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");
|
attributeMap.put("msid-semantic", " WMS my-media-stream");
|
||||||
|
|
||||||
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> entry :
|
for (final Map.Entry<String, DescriptionTransport<RtpDescription, IceUdpTransportInfo>>
|
||||||
contentMap.contents.entrySet()) {
|
entry : contentMap.contents.entrySet()) {
|
||||||
final String name = entry.getKey();
|
final String name = entry.getKey();
|
||||||
RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
|
checkNoWhitespace(name, "content name must not contain any whitespace");
|
||||||
RtpDescription description = descriptionTransport.description;
|
final DescriptionTransport<RtpDescription, IceUdpTransportInfo> descriptionTransport =
|
||||||
IceUdpTransportInfo transport = descriptionTransport.transport;
|
entry.getValue();
|
||||||
|
final RtpDescription description = descriptionTransport.description;
|
||||||
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
|
||||||
final String ufrag = transport.getAttribute("ufrag");
|
mediaAttributes.putAll(transportInfoMediaAttributes(descriptionTransport.transport));
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final ImmutableList.Builder<Integer> formatBuilder = new ImmutableList.Builder<>();
|
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();
|
final String id = payloadType.getId();
|
||||||
if (Strings.isNullOrEmpty(id)) {
|
if (Strings.isNullOrEmpty(id)) {
|
||||||
throw new IllegalArgumentException("Payload type is missing id");
|
throw new IllegalArgumentException("Payload type is missing id");
|
||||||
|
@ -353,6 +374,69 @@ public class SessionDescription {
|
||||||
return sessionDescriptionBuilder.createSessionDescription();
|
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) {
|
public static String checkNoWhitespace(final String input, final String message) {
|
||||||
if (CharMatcher.whitespace().matchesAnyOf(input)) {
|
if (CharMatcher.whitespace().matchesAnyOf(input)) {
|
||||||
throw new IllegalArgumentException(message);
|
throw new IllegalArgumentException(message);
|
||||||
|
@ -421,7 +505,7 @@ public class SessionDescription {
|
||||||
.append(' ')
|
.append(' ')
|
||||||
.append(media.protocol)
|
.append(media.protocol)
|
||||||
.append(' ')
|
.append(' ')
|
||||||
.append(Joiner.on(' ').join(media.formats))
|
.append(media.format)
|
||||||
.append(LINE_DIVIDER);
|
.append(LINE_DIVIDER);
|
||||||
s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
|
s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
|
||||||
appendAttributes(s, media.attributes);
|
appendAttributes(s, media.attributes);
|
||||||
|
@ -433,21 +517,21 @@ public class SessionDescription {
|
||||||
public final String media;
|
public final String media;
|
||||||
public final int port;
|
public final int port;
|
||||||
public final String protocol;
|
public final String protocol;
|
||||||
public final List<Integer> formats;
|
public final String format;
|
||||||
public final String connectionData;
|
public final String connectionData;
|
||||||
public final ArrayListMultimap<String, String> attributes;
|
public final Multimap<String, String> attributes;
|
||||||
|
|
||||||
public Media(
|
public Media(
|
||||||
String media,
|
String media,
|
||||||
int port,
|
int port,
|
||||||
String protocol,
|
String protocol,
|
||||||
List<Integer> formats,
|
String format,
|
||||||
String connectionData,
|
String connectionData,
|
||||||
ArrayListMultimap<String, String> attributes) {
|
Multimap<String, String> attributes) {
|
||||||
this.media = media;
|
this.media = media;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.protocol = protocol;
|
this.protocol = protocol;
|
||||||
this.formats = formats;
|
this.format = format;
|
||||||
this.connectionData = connectionData;
|
this.connectionData = connectionData;
|
||||||
this.attributes = attributes;
|
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 List<PeerConnection.IceServer> iceServers, final boolean trickle) {
|
||||||
final PeerConnection.RTCConfiguration rtcConfig =
|
final PeerConnection.RTCConfiguration rtcConfig =
|
||||||
new PeerConnection.RTCConfiguration(iceServers);
|
new PeerConnection.RTCConfiguration(iceServers);
|
||||||
|
@ -774,7 +774,7 @@ public class WebRTCWrapper {
|
||||||
void onRenegotiationNeeded();
|
void onRenegotiationNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract static class SetSdpObserver implements SdpObserver {
|
public abstract static class SetSdpObserver implements SdpObserver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
|
public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
|
||||||
|
@ -800,12 +800,12 @@ public class WebRTCWrapper {
|
||||||
|
|
||||||
public static class PeerConnectionNotInitialized extends IllegalStateException {
|
public static class PeerConnectionNotInitialized extends IllegalStateException {
|
||||||
|
|
||||||
private PeerConnectionNotInitialized() {
|
public PeerConnectionNotInitialized() {
|
||||||
super("initialize PeerConnection first");
|
super("initialize PeerConnection first");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class FailureToSetDescriptionException extends IllegalArgumentException {
|
public static class FailureToSetDescriptionException extends IllegalArgumentException {
|
||||||
public FailureToSetDescriptionException(String message) {
|
public FailureToSetDescriptionException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class Content extends Element {
|
public class Content extends Element {
|
||||||
|
|
||||||
public Content(final Creator creator, final Senders senders, final String name) {
|
public Content(final Creator creator, final Senders senders, final String name) {
|
||||||
|
@ -65,7 +65,7 @@ public class Content extends Element {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final String namespace = description.getNamespace();
|
final String namespace = description.getNamespace();
|
||||||
if (FileTransferDescription.NAMESPACES.contains(namespace)) {
|
if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
|
||||||
return FileTransferDescription.upgrade(description);
|
return FileTransferDescription.upgrade(description);
|
||||||
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
|
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
|
||||||
return RtpDescription.upgrade(description);
|
return RtpDescription.upgrade(description);
|
||||||
|
@ -90,9 +90,11 @@ public class Content extends Element {
|
||||||
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
|
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
|
||||||
return IbbTransportInfo.upgrade(transport);
|
return IbbTransportInfo.upgrade(transport);
|
||||||
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
|
} 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)) {
|
} else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
|
||||||
return IceUdpTransportInfo.upgrade(transport);
|
return IceUdpTransportInfo.upgrade(transport);
|
||||||
|
} else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) {
|
||||||
|
return WebRTCDataChannelTransportInfo.upgrade(transport);
|
||||||
} else if (transport != null) {
|
} else if (transport != null) {
|
||||||
return GenericTransportInfo.upgrade(transport);
|
return GenericTransportInfo.upgrade(transport);
|
||||||
} else {
|
} else {
|
||||||
|
@ -100,7 +102,6 @@ public class Content extends Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void setTransport(GenericTransportInfo transportInfo) {
|
public void setTransport(GenericTransportInfo transportInfo) {
|
||||||
this.addChild(transportInfo);
|
this.addChild(transportInfo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +1,233 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
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.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 eu.siacs.conversations.Config;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
|
||||||
import eu.siacs.conversations.entities.DownloadableFile;
|
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class FileTransferDescription extends GenericDescription {
|
public class FileTransferDescription extends GenericDescription {
|
||||||
|
|
||||||
public static List<String> NAMESPACES = Arrays.asList(
|
private FileTransferDescription() {
|
||||||
Version.FT_3.namespace,
|
super("description", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||||
Version.FT_4.namespace,
|
|
||||||
Version.FT_5.namespace
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
private FileTransferDescription(String name, String namespace) {
|
|
||||||
super(name, namespace);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Version getVersion() {
|
public static FileTransferDescription of(final File fileDescription) {
|
||||||
final String namespace = getNamespace();
|
final var description = new FileTransferDescription();
|
||||||
if (namespace.equals(Version.FT_3.namespace)) {
|
final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
|
||||||
return Version.FT_3;
|
file.addChild("name").setContent(fileDescription.name);
|
||||||
} else if (namespace.equals(Version.FT_4.namespace)) {
|
file.addChild("size").setContent(Long.toString(fileDescription.size));
|
||||||
return Version.FT_4;
|
if (fileDescription.mediaType != null) {
|
||||||
} else if (namespace.equals(Version.FT_5.namespace)) {
|
file.addChild("mediaType").setContent(fileDescription.mediaType);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
return description;
|
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) {
|
public static FileTransferDescription upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
|
Preconditions.checkArgument(
|
||||||
Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace");
|
"description".equals(element.getName()),
|
||||||
final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace());
|
"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.setAttributes(element.getAttributes());
|
||||||
description.setChildren(element.getChildren());
|
description.setChildren(element.getChildren());
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Version {
|
public static final class Checksum extends SessionInfo {
|
||||||
FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
|
public final List<Hash> hashes;
|
||||||
FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
|
|
||||||
FT_5("urn:xmpp:jingle:apps:file-transfer:5");
|
|
||||||
|
|
||||||
private final String namespace;
|
public Checksum(final String name, List<Hash> hashes) {
|
||||||
|
super(name);
|
||||||
Version(String namespace) {
|
this.hashes = hashes;
|
||||||
this.namespace = namespace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNamespace() {
|
@Override
|
||||||
return namespace;
|
@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) {
|
GenericDescription(String name, final String namespace) {
|
||||||
super(name, namespace);
|
super(name, namespace);
|
||||||
|
Preconditions.checkArgument("description".equals(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GenericDescription upgrade(final Element element) {
|
public static GenericDescription upgrade(final Element element) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ public class Group extends Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Group ofSdpString(final String input) {
|
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(" ");
|
final String[] parts = input.split(" ");
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
final String semantics = parts[0];
|
final String semantics = parts[0];
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
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.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
@ -23,16 +25,9 @@ public class IbbTransportInfo extends GenericTransportInfo {
|
||||||
return this.getAttribute("sid");
|
return this.getAttribute("sid");
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getBlockSize() {
|
public Long getBlockSize() {
|
||||||
final String blockSize = this.getAttribute("block-size");
|
final String blockSize = this.getAttribute("block-size");
|
||||||
if (blockSize == null) {
|
return Strings.isNullOrEmpty(blockSize) ? null : Longs.tryParse(blockSize);
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Integer.parseInt(blockSize);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IbbTransportInfo upgrade(final Element element) {
|
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.ImmutableCollection;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.transports.Transport;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
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() {
|
private Candidate() {
|
||||||
super("candidate");
|
super("candidate");
|
||||||
|
@ -396,7 +398,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
|
||||||
return fingerprint;
|
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 fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
|
||||||
final String setup = Iterables.getFirst(attributes.get("setup"), null);
|
final String setup = Iterables.getFirst(attributes.get("setup"), null);
|
||||||
if (setup != null && fingerprint != null) {
|
if (setup != null && fingerprint != null) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
package eu.siacs.conversations.xmpp.jingle.stanzas;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.google.common.base.CaseFormat;
|
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.base.Strings;
|
||||||
import com.google.common.collect.ImmutableMap;
|
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.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class JinglePacket extends IqPacket {
|
public class JinglePacket extends IqPacket {
|
||||||
|
|
||||||
private JinglePacket() {
|
private JinglePacket() {
|
||||||
|
@ -116,6 +121,39 @@ public class JinglePacket extends IqPacket {
|
||||||
jingle.addChild(child);
|
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() {
|
public String getSessionId() {
|
||||||
return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
|
return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
|
||||||
}
|
}
|
||||||
|
@ -153,7 +191,6 @@ public class JinglePacket extends IqPacket {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class ReasonWrapper {
|
public static class ReasonWrapper {
|
||||||
public final Reason reason;
|
public final Reason reason;
|
||||||
public final String text;
|
public final String text;
|
||||||
|
|
|
@ -18,7 +18,7 @@ public class Propose extends Element {
|
||||||
for (final Element child : this.children) {
|
for (final Element child : this.children) {
|
||||||
if ("description".equals(child.getName())) {
|
if ("description".equals(child.getName())) {
|
||||||
final String namespace = child.getNamespace();
|
final String namespace = child.getNamespace();
|
||||||
if (FileTransferDescription.NAMESPACES.contains(namespace)) {
|
if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
|
||||||
builder.add(FileTransferDescription.upgrade(child));
|
builder.add(FileTransferDescription.upgrade(child));
|
||||||
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
|
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
|
||||||
builder.add(RtpDescription.upgrade(child));
|
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