diff --git a/CHANGELOG.md b/CHANGELOG.md
index a105ca8b64518f3655232fbcbda1442ae2482769..0e9eb28bdb610c5be4c278f26231eca5a45b28ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+### Version 2.13.0
+
+* Easier access to 'Show QR code'
+* Support PEP Native Bookmarks
+* Add support for SDP Offer / Answer Model (Used by SIP gateways)
+* Raise target API to Android 14
+
### Version 2.12.12
* Support Private DNS (DNS over TLS)
diff --git a/build.gradle b/build.gradle
index 72bb7dedbb5e96356c59fe7a4dd77a57cd41d34e..2dbeb77d2efd4730dfec3d84c4c2fe6f4ee9e84f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.2.0-rc01'
+ classpath 'com.android.tools.build:gradle:8.2.0-rc03'
}
}
diff --git a/fastlane/metadata/android/en-US/changelogs/4208104.txt b/fastlane/metadata/android/en-US/changelogs/4208104.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a945cbcb3c6e709d27a27f42fa0d61c650bf7224
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/4208104.txt
@@ -0,0 +1,4 @@
+* Easier access to 'Show QR code'
+* Support PEP Native Bookmarks
+* Add support for SDP Offer / Answer Model (Used by SIP gateways)
+* Raise target API to Android 14
diff --git a/fastlane/metadata/android/es-ES/changelogs/4208104.txt b/fastlane/metadata/android/es-ES/changelogs/4208104.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dde4e4f058d1c6a09f4fd3bb44635756eff6ade2
--- /dev/null
+++ b/fastlane/metadata/android/es-ES/changelogs/4208104.txt
@@ -0,0 +1,4 @@
+* Acceso más fácil a "Mostrar el código QR
+* Soporte para marcadores nativos PEP
+* Añadir soporte para SDP Oferta / Respuesta Modelo (Utilizado por pasarelas SIP)
+* Aumento de la API de destino a Android 14
diff --git a/fastlane/metadata/android/gl-ES/changelogs/4208104.txt b/fastlane/metadata/android/gl-ES/changelogs/4208104.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f4532b6797151479e6086a6f92774ffd41b78906
--- /dev/null
+++ b/fastlane/metadata/android/gl-ES/changelogs/4208104.txt
@@ -0,0 +1,4 @@
+* Acceso mais rápido á 'Mostrar código QR'
+* Soporte para a PEP Marcadores Nativos
+* Engadido soporte para SDP Offer / Answer Model (usado por pasarelas SIP)
+* Establecida a API de Android 14 como obxectivo
diff --git a/fastlane/metadata/android/it-IT/changelogs/4208104.txt b/fastlane/metadata/android/it-IT/changelogs/4208104.txt
new file mode 100644
index 0000000000000000000000000000000000000000..00be75ede439405a86d9b989cd973df1e6eca5c8
--- /dev/null
+++ b/fastlane/metadata/android/it-IT/changelogs/4208104.txt
@@ -0,0 +1,4 @@
+* Accesso più facile a 'Mostra codice QR'
+* Supporto per PEP Native Bookmarks
+* Aggiunto supporto per il modello Offerta / Risposta SDP (usato dai gateway SIP)
+* Aumentata l'API di destinazione ad Android 14
diff --git a/fastlane/metadata/android/uk/changelogs/4208104.txt b/fastlane/metadata/android/uk/changelogs/4208104.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9235074b11e12a68e74ea5b6165a2f40f52ea25b
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/4208104.txt
@@ -0,0 +1,4 @@
+* Простіший доступ до «Показати QR-код»
+* Підтримка закладок PEP Native Bookmarks
+* Додано підтримку моделі SDP пропозиція/відповідь (Використовується шлюзами SIP)
+* Підвищено цільовий API до Android 14
diff --git a/fastlane/metadata/android/zh-CN/changelogs/398.txt b/fastlane/metadata/android/zh-CN/changelogs/398.txt
index 5a3df8a3ff6c4e670c57ca5bf4a723c6c2d475fb..3985836d3ae063f958c7e6316d695983c714f5aa 100644
--- a/fastlane/metadata/android/zh-CN/changelogs/398.txt
+++ b/fastlane/metadata/android/zh-CN/changelogs/398.txt
@@ -1,4 +1,4 @@
* 搜索个人对话
* 消息传递失败时通知用户
-* 重新启动时记住 Quicksy 用户的显示名称(昵称)
+* 重启时记住 Quicksy 用户的显示名称(昵称)
* 如有必要,添加按钮以从通知中启动 Orbot(Tor)
diff --git a/fastlane/metadata/android/zh-CN/changelogs/4207704.txt b/fastlane/metadata/android/zh-CN/changelogs/4207704.txt
index d07b29e61ee671b6673661da9e8c0f441038bd46..4c33d6b999f833dc5f368cc91281e56864a934e6 100644
--- a/fastlane/metadata/android/zh-CN/changelogs/4207704.txt
+++ b/fastlane/metadata/android/zh-CN/changelogs/4207704.txt
@@ -1,3 +1,3 @@
-* 支持私人 DNS (DNS over TLS)
+* 支持私人 DNS(DNS over TLS)
* 支持主题启动器图标
* 修复在 Android 11+ 上分享文件时罕见的权限问题
diff --git a/fastlane/metadata/android/zh-CN/changelogs/4208104.txt b/fastlane/metadata/android/zh-CN/changelogs/4208104.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7e5f80a1388f3abd11603e8c3299c70a948db70d
--- /dev/null
+++ b/fastlane/metadata/android/zh-CN/changelogs/4208104.txt
@@ -0,0 +1,4 @@
+* 更容易访问“显示二维码”
+* 支持 PEP Native Bookmarks
+* 添加对 SDP 请求/响应模型的支持(由 SIP 网关使用)
+* 将目标 API 提升到 Android 14
diff --git a/src/conversations/fastlane/metadata/android/eo/short_description.txt b/src/conversations/fastlane/metadata/android/eo/short_description.txt
new file mode 100644
index 0000000000000000000000000000000000000000..514cca5bdd23b8b7e514bd6b2e05121f6ad3dfec
--- /dev/null
+++ b/src/conversations/fastlane/metadata/android/eo/short_description.txt
@@ -0,0 +1 @@
+Ĉifrita, facile uzebla XMPP tujmesaĝilo por via poŝtelefono
diff --git a/src/conversations/fastlane/metadata/android/fi-FI/full_description.txt b/src/conversations/fastlane/metadata/android/fi-FI/full_description.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8071c92d06fc52f9232ffc84c9e9a69cd909eae8
--- /dev/null
+++ b/src/conversations/fastlane/metadata/android/fi-FI/full_description.txt
@@ -0,0 +1,39 @@
+Helppokäyttöinen, luotettava ja vähän akkua käyttävä. Sisäänrakennettu tuki kuville, ryhmille ja päästä päähän -salaukselle.
+
+Periaatteet:
+
+* Ole niin kaunis ja helppokäyttöinen kuin on mahdolista turvallisuudesta ja ykstyisyydestä tinkimättä
+* Käytä valmiita ja vakiintuneita protokollia
+* Älä riipu Google-tunnuksesta äläkä Google Cloud Messaging -palvelusta (GCM)
+* Vaadi niin vähän käyttöoikeuksia kuin mahdollista
+
+Ominaisuudet:
+
+* Päästä päähän -salaus joko OMEMO:lla tai OpenPGP:llä
+* Lähetä ja vastaanota kuvia
+* Salatut ääni- ja videopuhelut (DTLS-SRTP)
+* Intuitiivinen käyttöliittymä joka noudattaa Androidin muotoilukieltä
+* Profiilikuvat yhteystiedoille
+* Synkronoi työpöytäversion kanssa
+* Konferenssit (kirjanmerkkituella)
+* Osoitekirjaintegrointi
+* Useampi tili yhdessä näkymässä
+* Todella pieni akun kulutus
+
+Conversations:lla on helppo luoda tili conversations.im-palvelimella. Silti Conversations toimii myös minkä tahansa muun XMPP-palvelimen kanssa. Monia XMPP-palvelimia ylläpidetään ilmaiseksi vapaaehtoisvoimin.
+
+XMPP-ominaisuudet:
+
+Conversations toimii kaikkien XMPP-palvelinten kanssa. XMPP on kuitenkin laajennettava protokolla. Nämä laajennukset on standardoitu niin kutsuttuina XEP:inä. Conversations tukee muutamaa näistä tehdäkseen käyttäjäkokemuksesta paremman. On mahdollista että nykyinen XMPP-palvelimesi ei tue kaikkia näitä laajennoksia. Siispä saadaksesi kaiken ilon irti Conversationsista kannattaa harkita joko sellaiseen palvelimeen, joka tukee näitä, vaihtamista tai oman XMPP-palvelimen ylläpitämistä itsellesi ja kavereillesi.
+
+XEP:t ovat tällä hetkellä:
+
+* XEP-0065: SOCKS5 Bytestreams (tai mod_proxy65). Käytetään tiedostojen siirtoon jos molemmat osapuolet ovat palomuurin tai NAT:n takana.
+* XEP-0163: Personal Eventing Protocol profiilikuville
+* XEP-0191: Blocking command lets you blacklist spammers or block contacts without removing them from your roster.
+* XEP-0198: Stream Management mahdollistaa XMPP:n selviämisen pienestä verkon pätkimisestä ja TCP-yhteyden muutoksista.
+* XEP-0280: Kopiot lähettämistäsi viesteistä muille laitteillesi. Mahdolistaa laitteiden vaihdon kesken keskustelun täysin saumoitta.
+* XEP-0237: Roster Versioning säästää dataa heikoila yhteyksillä
+* XEP-0313: Message Archive Management synchronize message history with the server. Catch up with messages that were sent while Conversations was offline.
+* XEP-0352: Client State Indication lets the server know whether or not Conversations is in the background. Allows the server to save bandwidth by withholding unimportant packages.
+* XEP-0363: HTTP File Upload allows you to share files in conferences and with offline contacts. Requires an additional component on your server.
diff --git a/src/conversations/fastlane/metadata/android/fi-FI/short_description.txt b/src/conversations/fastlane/metadata/android/fi-FI/short_description.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2713b6efd11a4066561968fc0fc38e802281e007
--- /dev/null
+++ b/src/conversations/fastlane/metadata/android/fi-FI/short_description.txt
@@ -0,0 +1 @@
+Salattu ja helppokäyttöinen XMPP-pikaviestin mobiililaitteellesi
diff --git a/src/conversations/res/values-eo/strings.xml b/src/conversations/res/values-eo/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d5fc7d80eb682cb660b14fbfcc0e041d660652d4
--- /dev/null
+++ b/src/conversations/res/values-eo/strings.xml
@@ -0,0 +1,20 @@
+
+
+ Uzi conversations.im
+ Aliĝu al %1$s kaj babilu kun mi: %2$s
+ Vi estis invitita al %1$s. Ni gvidos vin tra la procezo de kreado de konto.
+\nElektante %1$s kiel provizanton vi povos komuniki kun uzantoj de aliaj provizantoj donante al ili vian plenan XMPP-adreson.
+ Ĉu vi jam havas XMPP-konton\? Ĉi tio povus esti la kazo se vi jam uzas alian XMPP-klienton aŭ antaŭe uzis Conversations. Se ne, vi povas krei novan XMPP-konton nun.
+\nKonsileto: Iuj retpoŝtaj provizantoj ankaŭ provizas XMPP-kontojn.
+ Se via kontakto estas proksime, ili ankaŭ povas skani la suban kodon por akcepti vian inviton.
+ Elekti vian XMPP-provizanton
+ Kunhavigi inviton kun…
+ XMPP estas provizanta sendependa tujmesaĝa reto. Vi povas uzi ĉi tiun klienton per kia ajn XMPP-servilo, kiun vi elektas.
+\nTamen por via komforto ni faciligis krei konton ĉe conversations.im; provizanto speciale taŭga por la uzo kun Conversations.
+ Vi estis invitita al %1$s. Uzantnomo jam estas elektita por vi. Ni gvidos vin tra la procezo de kreado de konto.
+\nVi povos komuniki kun uzantoj de aliaj provizantoj donante al ili vian plenan XMPP-adreson.
+ Nedece formatita provizokodo
+ Premu la kunhavigi butonon por sendi al via kontakto inviton al %1$s.
+ Via servila invito
+ Krei novan konton
+
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 4a0581f4cbea83603fd5d876e6a2302a60b9362d..c4ce7e6f0b53675cffe11f0dacc5e294721bf267 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -41,10 +41,11 @@ public final class Config {
public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US);
+ public static final boolean QUICK_LOG = false;
+
public static final Jid BUG_REPORTS = Jid.of("+14169938000@cheogram.com");
public static final Uri HELP = Uri.parse("https://cheogram.com");
-
public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
public static final String MAGIC_CREATE_DOMAIN = "chatterboxtown.us";
public static final Jid QUICKSY_DOMAIN = Jid.of("cheogram.com");
@@ -116,7 +117,7 @@ public final class Config {
public static final boolean OMEMO_PADDING = false;
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true;
- public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
+ public static final boolean DISABLE_PROXY_LOOKUP = false; //disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback)
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
index 9cf3d9091a6cf4d659fe91797e94817f6b73b120..3721f4cfeed67e57914529130bae30f63025823b 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -62,11 +62,13 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.jingle.DescriptionTransport;
import eu.siacs.conversations.xmpp.jingle.OmemoVerification;
import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap;
import eu.siacs.conversations.xmpp.jingle.RtpContentMap;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
@@ -1262,12 +1264,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (Config.REQUIRE_RTP_VERIFICATION) {
requireVerification(session);
}
- final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>();
+ final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>();
final OmemoVerification omemoVerification = new OmemoVerification();
omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId());
omemoVerification.setSessionFingerprint(session.getFingerprint());
- for (final Map.Entry content : rtpContentMap.contents.entrySet()) {
- final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
+ for (final Map.Entry> content : rtpContentMap.contents.entrySet()) {
+ final DescriptionTransport descriptionTransport = content.getValue();
final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo;
try {
encryptedTransportInfo = encrypt(descriptionTransport.transport, session);
@@ -1276,7 +1278,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
descriptionTransportBuilder.put(
content.getKey(),
- new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
+ new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo)
);
}
return Futures.immediateFuture(
@@ -1296,11 +1298,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) {
- final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>();
+ final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>();
final OmemoVerification omemoVerification = new OmemoVerification();
final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>();
- for (final Map.Entry content : omemoVerifiedRtpContentMap.contents.entrySet()) {
- final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue();
+ for (final Map.Entry> content : omemoVerifiedRtpContentMap.contents.entrySet()) {
+ final DescriptionTransport descriptionTransport = content.getValue();
final OmemoVerifiedPayload decryptedTransport;
try {
decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures);
@@ -1310,7 +1312,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
omemoVerification.setOrEnsureEqual(decryptedTransport);
descriptionTransportBuilder.put(
content.getKey(),
- new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
+ new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload)
);
}
processPostponed();
@@ -1376,18 +1378,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
));
}
- public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
- if (buildHeader(axolotlMessage, conversation)) {
- onMessageCreatedCallback.run(axolotlMessage);
- } else {
- onMessageCreatedCallback.run(null);
- }
+ public ListenableFuture prepareKeyTransportMessage(final Conversation conversation) {
+ return Futures.submit(()->{
+ final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
+ if (buildHeader(axolotlMessage, conversation)) {
+ return axolotlMessage;
+ } else {
+ throw new IllegalStateException("No session to decrypt to");
}
- });
+ },executor);
}
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
index f4e2b3b931c0d6b19cc2b2a32b8ed13db221ce35..f513947599b4daddd23998dcbe2b5abfcc8ef641 100644
--- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -27,11 +27,7 @@ public abstract class AbstractGenerator {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private final String[] FEATURES = {
Namespace.JINGLE,
-
- //Jingle File Transfer
- FileTransferDescription.Version.FT_3.getNamespace(),
- FileTransferDescription.Version.FT_4.getNamespace(),
- FileTransferDescription.Version.FT_5.getNamespace(),
+ Namespace.JINGLE_APPS_FILE_TRANSFER,
Namespace.JINGLE_TRANSPORTS_S5B,
Namespace.JINGLE_TRANSPORTS_IBB,
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
@@ -126,6 +122,7 @@ public abstract class AbstractGenerator {
if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
features.addAll(Arrays.asList(VOIP_NAMESPACES));
+ features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
}
if (mXmppConnectionService.broadcastLastActivity()) {
features.add(Namespace.IDLE);
diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java
index f36506bd1db43a27fff4ad2264f226cd92e35d66..fd7b27db4a2f1a3bebe6f03a0a84454b08a19cc7 100644
--- a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java
+++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java
@@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList;
import org.jetbrains.annotations.NotNull;
+import java.util.ArrayList;
import java.util.List;
import eu.siacs.conversations.Config;
@@ -129,6 +130,23 @@ public class UnifiedPushDatabase extends SQLiteOpenHelper {
return null;
}
+ public List deletePushTargets() {
+ final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
+ final ImmutableList.Builder builder = new ImmutableList.Builder<>();
+ try (final Cursor cursor = sqLiteDatabase.query("push",new String[]{"application","instance"},null,null,null,null,null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ builder.add(new PushTarget(
+ cursor.getString(cursor.getColumnIndexOrThrow("application")),
+ cursor.getString(cursor.getColumnIndexOrThrow("instance"))));
+ }
+ } catch (final Exception e) {
+ Log.d(Config.LOGTAG,"unable to retrieve push targets",e);
+ return builder.build();
+ }
+ sqLiteDatabase.delete("push",null,null);
+ return builder.build();
+ }
+
public boolean hasEndpoints(final UnifiedPushBroker.Transport transport) {
final SQLiteDatabase sqLiteDatabase = getReadableDatabase();
try (final Cursor cursor =
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
index f27bf7fc52a0920910019b49925a359118de544b..2562e13a62a22d80edfc7e4bbe7d7a17a40ce5e3 100644
--- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
@@ -1,5 +1,6 @@
package eu.siacs.conversations.services;
+import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -9,10 +10,18 @@ import android.os.Messenger;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
+
+import androidx.annotation.NonNull;
+
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding;
+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 eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -87,14 +96,32 @@ public class UnifiedPushBroker {
if (transport.account.isEnabled()) {
renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger);
} else {
+ if (pushTargetMessenger.messenger != null) {
+ sendRegistrationDelayed(pushTargetMessenger.messenger,"account is disabled");
+ }
Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
}
} else {
+ if (pushTargetMessenger.messenger != null) {
+ sendRegistrationDelayed(pushTargetMessenger.messenger,"no transport selected");
+ }
Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
}
return transportOptional;
}
+ private void sendRegistrationDelayed(final Messenger messenger, final String error) {
+ final Intent intent = new Intent(UnifiedPushDistributor.ACTION_REGISTRATION_DELAYED);
+ intent.putExtra(UnifiedPushDistributor.EXTRA_MESSAGE, error);
+ final var message = new Message();
+ message.obj = intent;
+ try {
+ messenger.send(message);
+ } catch (final RemoteException e) {
+ Log.d(Config.LOGTAG,"unable to tell messenger of delayed registration",e);
+ }
+ }
+
private void renewUnifiedEndpoint(final Transport transport, final PushTargetMessenger pushTargetMessenger) {
final Account account = transport.account;
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
@@ -112,6 +139,7 @@ public class UnifiedPushBroker {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
+ UnifiedPushDistributor.quickLog(service,String.format("%s: try to renew UnifiedPush %s", account.getJid(), renewal.toString()));
final String hashedApplication =
UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
final String hashedInstance =
@@ -186,7 +214,16 @@ public class UnifiedPushBroker {
+ renewal.instance
+ " was updated to "
+ endpoint);
- final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint = new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
+ UnifiedPushDistributor.quickLog(
+ service,
+ "endpoint for "
+ + renewal.application
+ + "/"
+ + renewal.instance
+ + " was updated to "
+ + endpoint);
+ final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint =
+ new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
sendEndpoint(messenger, renewal.instance, applicationEndpoint);
}
}
@@ -210,6 +247,9 @@ public class UnifiedPushBroker {
public boolean reconfigurePushDistributor() {
final boolean enabled = getTransport().isPresent();
setUnifiedPushDistributorEnabled(enabled);
+ if (!enabled) {
+ unregisterCurrentPushTargets();
+ }
return enabled;
}
@@ -232,6 +272,43 @@ public class UnifiedPushBroker {
}
}
+ private void unregisterCurrentPushTargets() {
+ final var future = deletePushTargets();
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(
+ final List pushTargets) {
+ broadcastUnregistered(pushTargets);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ "could not delete endpoints after UnifiedPushDistributor was disabled");
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture> deletePushTargets() {
+ return Futures.submit(() -> UnifiedPushDatabase.getInstance(service).deletePushTargets(),SCHEDULER);
+ }
+
+ private void broadcastUnregistered(final List pushTargets) {
+ for(final UnifiedPushDatabase.PushTarget pushTarget : pushTargets) {
+ Log.d(Config.LOGTAG,"sending unregistered to "+pushTarget);
+ broadcastUnregistered(pushTarget);
+ }
+ }
+
+ private void broadcastUnregistered(final UnifiedPushDatabase.PushTarget pushTarget) {
+ final var intent = unregisteredIntent(pushTarget);
+ service.sendBroadcast(intent);
+ }
+
public boolean processPushMessage(
final Account account, final Jid transport, final Element push) {
final String instance = push.getAttribute("instance");
@@ -326,6 +403,12 @@ public class UnifiedPushBroker {
updateIntent.putExtra("token", target.instance);
updateIntent.putExtra("bytesMessage", payload);
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
+ 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);
}
@@ -336,11 +419,30 @@ public class UnifiedPushBroker {
service.sendBroadcast(updateIntent);
}
- private static Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
+ private Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
intent.setPackage(endpoint.application);
intent.putExtra("token", instance);
intent.putExtra("endpoint", endpoint.endpoint);
+ final var distributorVerificationIntent = new Intent();
+ distributorVerificationIntent.setPackage(service.getPackageName());
+ final var pendingIntent =
+ PendingIntent.getBroadcast(
+ service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
+ intent.putExtra("distributor", pendingIntent);
+ return intent;
+ }
+
+ private Intent unregisteredIntent(final UnifiedPushDatabase.PushTarget pushTarget) {
+ final Intent intent = new Intent(UnifiedPushDistributor.ACTION_UNREGISTERED);
+ intent.setPackage(pushTarget.application);
+ intent.putExtra("token", pushTarget.instance);
+ final var distributorVerificationIntent = new Intent();
+ distributorVerificationIntent.setPackage(service.getPackageName());
+ final var pendingIntent =
+ PendingIntent.getBroadcast(
+ service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
+ intent.putExtra("distributor", pendingIntent);
return intent;
}
@@ -368,7 +470,7 @@ public class UnifiedPushBroker {
public static class PushTargetMessenger {
private final UnifiedPushDatabase.PushTarget pushTarget;
- private final Messenger messenger;
+ public final Messenger messenger;
public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) {
this.pushTarget = pushTarget;
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
index c5402dec6b2fa3eab65f69c2beae6bb3cb493f1b..b47a61a531fdc5b4afcadd2f540acf8b803000c3 100644
--- a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
@@ -6,8 +6,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Message;
import android.os.Messenger;
import android.os.Parcelable;
+import android.os.RemoteException;
import android.util.Log;
import com.google.common.base.Charsets;
@@ -27,16 +29,30 @@ import eu.siacs.conversations.utils.Compatibility;
public class UnifiedPushDistributor extends BroadcastReceiver {
+ // distributor actions (these are actios used for connector->distributor broadcasts)
+ // we, the distributor, have a broadcast receiver listening for those actions
+
public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER";
public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER";
+
+
+ // connector actions (these are actions used for distributor->connector broadcasts)
+ public static final String ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED";
public static final String ACTION_BYTE_MESSAGE =
"org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
public static final String ACTION_REGISTRATION_FAILED =
"org.unifiedpush.android.connector.REGISTRATION_FAILED";
+
+ // this action is only used in 'messenger' communication to tell the app that a registration is
+ // probably fine but can not be processed right now; for example due to spotty internet
+ public static final String ACTION_REGISTRATION_DELAYED =
+ "org.unifiedpush.android.connector.REGISTRATION_DELAYED";
public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE";
public static final String ACTION_NEW_ENDPOINT =
"org.unifiedpush.android.connector.NEW_ENDPOINT";
+ public static final String EXTRA_MESSAGE = "message";
+
public static final String PREFERENCE_ACCOUNT = "up_push_account";
public static final String PREFERENCE_PUSH_SERVER = "up_push_server";
@@ -50,9 +66,8 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
}
final String action = intent.getAction();
final String application;
- final Parcelable appByPendingIntent = intent.getParcelableExtra("app");
- if (appByPendingIntent instanceof PendingIntent) {
- final PendingIntent pendingIntent = (PendingIntent) appByPendingIntent;
+ final Parcelable appVerification = intent.getParcelableExtra("app");
+ if (appVerification instanceof PendingIntent pendingIntent) {
application = pendingIntent.getIntentSender().getCreatorPackage();
Log.d(Config.LOGTAG,"received application name via pending intent "+ application);
} else {
@@ -62,18 +77,12 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
final String instance = intent.getStringExtra("token");
final List features = intent.getStringArrayListExtra("features");
switch (Strings.nullToEmpty(action)) {
- case ACTION_REGISTER:
- register(context, application, instance, features, messenger);
- break;
- case ACTION_UNREGISTER:
- unregister(context, instance);
- break;
- case Intent.ACTION_PACKAGE_FULLY_REMOVED:
- unregisterApplication(context, intent.getData());
- break;
- default:
- Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
- break;
+ case ACTION_REGISTER -> register(context, application, instance, features, messenger);
+ case ACTION_UNREGISTER -> unregister(context, instance);
+ case Intent.ACTION_PACKAGE_FULLY_REMOVED ->
+ unregisterApplication(context, intent.getData());
+ default ->
+ Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
}
}
@@ -102,6 +111,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
Log.d(
Config.LOGTAG,
"successfully created UnifiedPush entry. waking up XmppConnectionService");
+ quickLog(context, String.format("successfully registered %s (token = %s) for UnifiedPushed", application, instance));
final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
serviceIntent.putExtra("instance", instance);
@@ -113,11 +123,25 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
} else {
Log.d(Config.LOGTAG, "not successful. sending error message back to application");
final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
+ registrationFailed.putExtra(EXTRA_MESSAGE, "instance already exits");
registrationFailed.setPackage(application);
registrationFailed.putExtra("token", instance);
- context.sendBroadcast(registrationFailed);
+ if (messenger instanceof Messenger m) {
+ final var message = new Message();
+ message.obj = registrationFailed;
+ try {
+ m.send(message);
+ } catch (final RemoteException e) {
+ context.sendBroadcast(registrationFailed);
+ }
+ } else {
+ context.sendBroadcast(registrationFailed);
+ }
}
} else {
+ if (messenger instanceof Messenger m) {
+ sendRegistrationFailed(m,"Your application is not registered to receive messages");
+ }
Log.d(
Config.LOGTAG,
"ignoring invalid UnifiedPush registration. Unknown application "
@@ -125,6 +149,18 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
}
}
+ private void sendRegistrationFailed(final Messenger messenger, final String error) {
+ final Intent intent = new Intent(ACTION_REGISTRATION_FAILED);
+ intent.putExtra(EXTRA_MESSAGE, error);
+ final var message = new Message();
+ message.obj = intent;
+ try {
+ messenger.send(message);
+ } catch (final RemoteException e) {
+ Log.d(Config.LOGTAG,"unable to tell messenger of failed registration",e);
+ }
+ }
+
private List getBroadcastReceivers(final Context context, final String application) {
final Intent messageIntent = new Intent(ACTION_MESSAGE);
messageIntent.setPackage(application);
@@ -141,7 +177,9 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
}
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
if (unifiedPushDatabase.deleteInstance(instance)) {
+ quickLog(context, String.format("successfully unregistered token %s from UnifiedPushed (application requested unregister)", instance));
Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
+ // TODO send UNREGISTERED broadcast back to app?!
}
}
@@ -154,6 +192,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
if (database.deleteApplication(application)) {
+ quickLog(context, String.format("successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)", application));
Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
}
}
@@ -166,4 +205,11 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
.hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
.asBytes());
}
+
+ public static void quickLog(final Context context, final String message) {
+ final Intent intent = new Intent(context, XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_QUICK_LOG);
+ intent.putExtra("message", message);
+ context.startService(intent);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index f4b1b0ffb1d9b395765541b71e1b0551cfd71a6e..6d90180640f89a87cb6488f556b72e4d84c604b8 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -153,6 +153,7 @@ import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
+import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -227,6 +228,7 @@ public class XmppConnectionService extends Service {
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
+ public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
@@ -1016,6 +1018,12 @@ public class XmppConnectionService extends Service {
case ACTION_FCM_MESSAGE_RECEIVED:
Log.d(Config.LOGTAG, "push message arrived in service. account");
break;
+ case ACTION_QUICK_LOG:
+ final String message = intent == null ? null : intent.getStringExtra("message");
+ if (message != null && Config.QUICK_LOG) {
+ quickLog(message);
+ }
+ break;
case Intent.ACTION_SEND:
final Uri uri = intent == null ? null : intent.getData();
if (uri != null) {
@@ -1036,6 +1044,23 @@ public class XmppConnectionService extends Service {
return START_STICKY;
}
+ private void quickLog(final String message) {
+ if (Strings.isNullOrEmpty(message)) {
+ return;
+ }
+ final Account account = AccountUtils.getFirstEnabled(this);
+ if (account == null) {
+ return;
+ }
+ final Conversation conversation =
+ findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
+ final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE);
+ report.setStatus(Message.STATUS_RECEIVED);
+ conversation.add(report);
+ databaseBackend.createMessage(report);
+ updateConversationUi();
+ }
+
private void manageAccountConnectionStatesInternal() {
manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
index 1d87efdac43c4ed7b2e4416d539c34d0993a340f..3cd197deda24a233f1424582ac0d68ee0de4111f 100644
--- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
@@ -1,8 +1,9 @@
package eu.siacs.conversations.ui;
-import static java.util.Arrays.asList;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
+import static java.util.Arrays.asList;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -65,7 +66,6 @@ import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.ContentAddition;
@@ -75,6 +75,16 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
+import org.webrtc.RendererCommon;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoTrack;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
public class RtpSessionActivity extends XmppActivity
implements XmppConnectionService.OnJingleRtpConnectionUpdate,
eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
@@ -109,9 +119,7 @@ public class RtpSessionActivity extends XmppActivity
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD);
private static final List STATES_CONSIDERED_CONNECTED =
- Arrays.asList(
- RtpEndUserState.CONNECTED,
- RtpEndUserState.RECONNECTING);
+ Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
private static final List STATES_SHOWING_PIP_PLACEHOLDER =
Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
@@ -262,21 +270,22 @@ public class RtpSessionActivity extends XmppActivity
}
public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_help:
- launchHelpInBrowser();
- return true;
- case R.id.action_goto_chat:
- switchToConversation();
- return true;
- case R.id.action_dialpad:
- toggleDialpadVisibility();
- return true;
- case R.id.action_switch_to_video:
- requestPermissionAndSwitchToVideo();
- return true;
+ final var itemItem = item.getItemId();
+ if (itemItem == R.id.action_help) {
+ launchHelpInBrowser();
+ return true;
+ } else if (itemItem == R.id.action_goto_chat) {
+ switchToConversation();
+ return true;
+ } else if (itemItem == R.id.action_dialpad) {
+ toggleDialpadVisibility();
+ return true;
+ } else if (itemItem == R.id.action_switch_to_video) {
+ requestPermissionAndSwitchToVideo();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
}
- return super.onOptionsItemSelected(item);
}
private void launchHelpInBrowser() {
@@ -353,8 +362,9 @@ public class RtpSessionActivity extends XmppActivity
}
private void acceptContentAdd(final ContentAddition contentAddition) {
- if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) {
- Log.d(Config.LOGTAG,"ignore press on content-accept button");
+ if (contentAddition == null
+ || contentAddition.direction != ContentAddition.Direction.INCOMING) {
+ Log.d(Config.LOGTAG, "ignore press on content-accept button");
return;
}
requestPermissionAndAcceptContentAdd(contentAddition);
@@ -363,7 +373,11 @@ public class RtpSessionActivity extends XmppActivity
private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
final List permissions = permissions(contentAddition.media());
if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
- requireRtpConnection().acceptContentAdd(contentAddition.summary);
+ try {
+ requireRtpConnection().acceptContentAdd(contentAddition.summary);
+ } catch (final IllegalStateException e) {
+ Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
}
}
@@ -715,8 +729,10 @@ public class RtpSessionActivity extends XmppActivity
private boolean isConnected() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
- final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState();
- return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
+ final RtpEndUserState endUserState =
+ connection == null ? null : connection.getEndUserState();
+ return STATES_CONSIDERED_CONNECTED.contains(endUserState)
+ || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
}
private boolean switchToPictureInPicture() {
@@ -875,68 +891,43 @@ public class RtpSessionActivity extends XmppActivity
updateStateDisplay(state, Collections.emptySet(), null);
}
- private void updateStateDisplay(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) {
+ private void updateStateDisplay(
+ final RtpEndUserState state,
+ final Set media,
+ final ContentAddition contentAddition) {
switch (state) {
- case INCOMING_CALL:
+ case INCOMING_CALL -> {
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
if (media.contains(Media.VIDEO)) {
setTitle(R.string.rtp_state_incoming_video_call);
} else {
setTitle(R.string.rtp_state_incoming_call);
}
- break;
- case INCOMING_CONTENT_ADD:
+ }
+ case INCOMING_CONTENT_ADD -> {
if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
setTitle(R.string.rtp_state_content_add_video);
} else {
setTitle(R.string.rtp_state_content_add);
}
- break;
- case CONNECTING:
- setTitle(R.string.rtp_state_connecting);
- break;
- case CONNECTED:
- setTitle(R.string.rtp_state_connected);
- break;
- case RECONNECTING:
- setTitle(R.string.rtp_state_reconnecting);
- break;
- case ACCEPTING_CALL:
- setTitle(R.string.rtp_state_accepting_call);
- break;
- case ENDING_CALL:
- setTitle(R.string.rtp_state_ending_call);
- break;
- case FINDING_DEVICE:
- setTitle(R.string.rtp_state_finding_device);
- break;
- case RINGING:
- setTitle(R.string.rtp_state_ringing);
- break;
- case DECLINED_OR_BUSY:
- setTitle(R.string.rtp_state_declined_or_busy);
- break;
- case CONNECTIVITY_ERROR:
- setTitle(R.string.rtp_state_connectivity_error);
- break;
- case CONNECTIVITY_LOST_ERROR:
- setTitle(R.string.rtp_state_connectivity_lost_error);
- break;
- case RETRACTED:
- setTitle(R.string.rtp_state_retracted);
- break;
- case APPLICATION_ERROR:
- setTitle(R.string.rtp_state_application_failure);
- break;
- case SECURITY_ERROR:
- setTitle(R.string.rtp_state_security_error);
- break;
- case ENDED:
- throw new IllegalStateException(
- "Activity should have called finishAndReleaseWakeLock();");
- default:
- throw new IllegalStateException(
- String.format("State %s has not been handled in UI", state));
+ }
+ case CONNECTING -> setTitle(R.string.rtp_state_connecting);
+ case CONNECTED -> setTitle(R.string.rtp_state_connected);
+ case RECONNECTING -> setTitle(R.string.rtp_state_reconnecting);
+ case ACCEPTING_CALL -> setTitle(R.string.rtp_state_accepting_call);
+ case ENDING_CALL -> setTitle(R.string.rtp_state_ending_call);
+ case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device);
+ case RINGING -> setTitle(R.string.rtp_state_ringing);
+ case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy);
+ case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error);
+ case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error);
+ case RETRACTED -> setTitle(R.string.rtp_state_retracted);
+ case APPLICATION_ERROR -> setTitle(R.string.rtp_state_application_failure);
+ case SECURITY_ERROR -> setTitle(R.string.rtp_state_security_error);
+ case ENDED -> throw new IllegalStateException(
+ "Activity should have called finishAndReleaseWakeLock();");
+ default -> throw new IllegalStateException(
+ String.format("State %s has not been handled in UI", state));
}
}
@@ -992,7 +983,10 @@ public class RtpSessionActivity extends XmppActivity
}
@SuppressLint("RestrictedApi")
- private void updateButtonConfiguration(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) {
+ private void updateButtonConfiguration(
+ final RtpEndUserState state,
+ final Set media,
+ final ContentAddition contentAddition) {
if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
this.binding.rejectCall.setVisibility(View.INVISIBLE);
this.binding.endCall.setVisibility(View.INVISIBLE);
@@ -1008,7 +1002,8 @@ public class RtpSessionActivity extends XmppActivity
this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
- this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video));
+ this.binding.rejectCall.setContentDescription(
+ getString(R.string.reject_switch_to_video));
this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.rejectCall.setVisibility(View.VISIBLE);
@@ -1100,7 +1095,7 @@ public class RtpSessionActivity extends XmppActivity
private void updateInCallButtonConfigurationSpeaker(
final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
switch (selectedAudioDevice) {
- case EARPIECE:
+ case EARPIECE -> {
this.binding.inCallActionRight.setImageResource(
R.drawable.ic_volume_off_black_24dp);
if (numberOfChoices >= 2) {
@@ -1109,13 +1104,13 @@ public class RtpSessionActivity extends XmppActivity
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
}
- break;
- case WIRED_HEADSET:
+ }
+ case WIRED_HEADSET -> {
this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
- break;
- case SPEAKER_PHONE:
+ }
+ case SPEAKER_PHONE -> {
this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
if (numberOfChoices >= 2) {
this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
@@ -1123,13 +1118,13 @@ public class RtpSessionActivity extends XmppActivity
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
}
- break;
- case BLUETOOTH:
+ }
+ case BLUETOOTH -> {
this.binding.inCallActionRight.setImageResource(
R.drawable.ic_bluetooth_audio_black_24dp);
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
- break;
+ }
}
this.binding.inCallActionRight.setVisibility(View.VISIBLE);
}
@@ -1158,10 +1153,10 @@ public class RtpSessionActivity extends XmppActivity
private void switchCamera(final View view) {
Futures.addCallback(
requireRtpConnection().switchCamera(),
- new FutureCallback() {
+ new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Boolean isFrontCamera) {
- binding.localVideo.setMirror(isFrontCamera);
+ binding.localVideo.setMirror(Boolean.TRUE.equals(isFrontCamera));
}
@Override
@@ -1526,8 +1521,7 @@ public class RtpSessionActivity extends XmppActivity
final Account account, Jid with, final RtpEndUserState state, final Set media) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
- if (RtpCapability.jmiSupport(account.getRoster()
- .getContact(with))) {
+ if (RtpCapability.jmiSupport(account.getRoster().getContact(with))) {
intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
} else {
intent.putExtra(EXTRA_WITH, with.toEscapedString());
diff --git a/src/main/java/eu/siacs/conversations/utils/Checksum.java b/src/main/java/eu/siacs/conversations/utils/Checksum.java
deleted file mode 100644
index 407cb1944c8aaf283a1cc40dbf75d970680c8a4f..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/utils/Checksum.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java
index eb440f51375f31888ea1f30b64d7e594d6e319b8..8afd52c4c8daa6f26c51e3cde502c18cbb214b72 100644
--- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java
+++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java
@@ -29,7 +29,7 @@ public class LocalizedContent {
final String childLanguage = child.getAttribute("xml:lang");
final String lang = childLanguage == null ? parentLanguage : childLanguage;
final String content = child.getContent();
- if (namespace == null || "jabber:client".equals(namespace)) {
+ if (content != null && (namespace == null || Namespace.JABBER_CLIENT.equals(namespace))) {
if (contents.put(lang, content == null ? "" : content) != null) {
//anything that has multiple contents for the same language is invalid
return null;
diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java
index 27bc38dce3e3be6fe2e7b33d3f360f3993e455d6..87cf2167a3f09aadcb4c76816c50766ea77a3add 100644
--- a/src/main/java/eu/siacs/conversations/xml/Namespace.java
+++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java
@@ -2,6 +2,7 @@ package eu.siacs.conversations.xml;
public final class Namespace {
public static final String STREAMS = "http://etherx.jabber.org/streams";
+ public static final String JABBER_CLIENT = "jabber:client";
public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2";
@@ -46,7 +47,11 @@ public final class Namespace {
public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1";
public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1";
public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1";
+ public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1";
+ public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1";
public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1";
+
+ public static final String JINGLE_APPS_FILE_TRANSFER = "urn:xmpp:jingle:apps:file-transfer:5";
public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0";
public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0";
public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
@@ -71,4 +76,5 @@ public final class Namespace {
public static final String REPORTING = "urn:xmpp:reporting:1";
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
+ public static final String HASHES = "urn:xmpp:hashes:2";
}
diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java
index db2b111727227823eb46a0e662598844fdffc688..d8d98348e592c5b136847dbc7be4cb82be320220 100644
--- a/src/main/java/eu/siacs/conversations/xml/Tag.java
+++ b/src/main/java/eu/siacs/conversations/xml/Tag.java
@@ -43,6 +43,10 @@ public class Tag {
return name;
}
+ public String identifier() {
+ return String.format("%s#%s", name, this.attributes.get("xmlns"));
+ }
+
public String getAttribute(final String attrName) {
return this.attributes.get(attrName);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 93b4ba32775feebb80b67e8e1382c41fe02a63bf..02726486e6dd4f82bd962611483a1ee843b2823b 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -17,8 +17,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
+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.ImmutableList;
import org.xmlpull.v1.XmlPullParserException;
@@ -169,7 +172,7 @@ public class XmppConnection implements Runnable {
private boolean isBound = false;
private Element streamFeatures;
private Element boundStreamFeatures;
- private String streamId = null;
+ private StreamId streamId = null;
private int stanzasReceived = 0;
private int stanzasSent = 0;
private int stanzasSentBeforeAuthentication;
@@ -192,7 +195,7 @@ public class XmppConnection implements Runnable {
private OnStatusChanged statusListener = null;
private OnBindListener bindListener = null;
private OnMessageAcknowledged acknowledgedListener = null;
- private SaslMechanism saslMechanism;
+ private LoginInfo loginInfo;
private HashedToken.Mechanism hashTokenRequest;
private HttpUrl redirectionUrl = null;
private String verifiedHostname = null;
@@ -382,6 +385,12 @@ public class XmppConnection implements Runnable {
+ storedBackupResult);
}
}
+ final StreamId streamId = this.streamId;
+ final Resolver.Result resumeLocation = streamId == null ? null : streamId.location;
+ if (resumeLocation != null) {
+ Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected resume location on position 0");
+ results.add(0, resumeLocation);
+ }
final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
if (seeOtherHost != null) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0");
@@ -599,7 +608,6 @@ public class XmppConnection implements Runnable {
if (processSuccess(success)) {
break;
}
-
} else if (nextTag.isStart("failure", Namespace.TLS)) {
throw new StateChangingException(Account.State.TLS_ERROR);
} else if (nextTag.isStart("failure")) {
@@ -609,7 +617,7 @@ public class XmppConnection implements Runnable {
// two step sasl2 - we don’t support this yet
throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
} else if (nextTag.isStart("challenge")) {
- if (isSecure() && this.saslMechanism != null) {
+ if (isSecure() && this.loginInfo != null) {
final Element challenge = tagReader.readElement(nextTag);
processChallenge(challenge);
} else {
@@ -622,10 +630,10 @@ public class XmppConnection implements Runnable {
} else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
final Element enabled = tagReader.readElement(nextTag);
processEnabled(enabled);
- } else if (nextTag.isStart("resumed")) {
+ } else if (nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
final Element resumed = tagReader.readElement(nextTag);
processResumed(resumed);
- } else if (nextTag.isStart("r")) {
+ } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
tagReader.readElement(nextTag);
if (Config.EXTENDED_SM_LOGGING) {
Log.d(
@@ -636,7 +644,7 @@ public class XmppConnection implements Runnable {
}
final AckPacket ack = new AckPacket(this.stanzasReceived);
tagWriter.writeStanzaAsync(ack);
- } else if (nextTag.isStart("a")) {
+ } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) {
boolean accountUiNeedsRefresh = false;
synchronized (NotificationService.CATCHUP_LOCK) {
if (mWaitingForSmCatchup.compareAndSet(true, false)) {
@@ -679,15 +687,22 @@ public class XmppConnection implements Runnable {
if (acknowledgedMessages) {
mXmppConnectionService.updateConversationUi();
}
- } else if (nextTag.isStart("failed")) {
+ } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
final Element failed = tagReader.readElement(nextTag);
processFailed(failed, true);
- } else if (nextTag.isStart("iq")) {
+ } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
processIq(nextTag);
- } else if (nextTag.isStart("message")) {
+ } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
processMessage(nextTag);
- } else if (nextTag.isStart("presence")) {
+ } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
processPresence(nextTag);
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": Encountered unknown stream element"
+ + nextTag.identifier());
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
nextTag = tagReader.readTag();
}
@@ -712,7 +727,7 @@ public class XmppConnection implements Runnable {
throw new AssertionError("Missing implementation for " + version);
}
try {
- response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket)));
+ response.setContent(this.loginInfo.saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket)));
} catch (final SaslMechanism.AuthenticationException e) {
// TODO: Send auth abort tag.
Log.e(Config.LOGTAG, e.toString());
@@ -729,8 +744,9 @@ public class XmppConnection implements Runnable {
} catch (final IllegalArgumentException e) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
- final SaslMechanism currentSaslMechanism = this.saslMechanism;
- if (currentSaslMechanism == null) {
+ final LoginInfo currentLoginInfo = this.loginInfo;
+ final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
+ if (currentLoginInfo == null || currentSaslMechanism == null) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
final String challenge;
@@ -829,13 +845,25 @@ public class XmppConnection implements Runnable {
//if we did not enable stream management in bind do it now
waitForDisco = enableStreamManagement();
}
+ final boolean negotiatedCarbons;
if (carbonsEnabled != null) {
+ negotiatedCarbons = true;
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid() + ": successfully enabled carbons");
+ account.getJid().asBareJid()
+ + ": successfully enabled carbons (via Bind 2.0)");
features.carbonsEnabled = true;
+ } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
+ negotiatedCarbons = true;
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": successfully enabled carbons (via Bind 2.0/implicit)");
+ features.carbonsEnabled = true;
+ } else {
+ negotiatedCarbons = false;
}
- sendPostBindInitialization(waitForDisco, carbonsEnabled != null);
+ sendPostBindInitialization(waitForDisco, negotiatedCarbons);
}
final HashedToken.Mechanism tokenMechanism;
if (SaslMechanism.hashedToken(currentSaslMechanism)) {
@@ -939,7 +967,7 @@ public class XmppConnection implements Runnable {
}
Log.d(Config.LOGTAG, failure.toString());
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
- if (SaslMechanism.hashedToken(this.saslMechanism)) {
+ if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token");
account.resetFastToken();
mXmppConnectionService.databaseBackend.updateAccount(account);
@@ -965,7 +993,7 @@ public class XmppConnection implements Runnable {
}
}
}
- if (SaslMechanism.hashedToken(this.saslMechanism)) {
+ if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@@ -985,18 +1013,27 @@ public class XmppConnection implements Runnable {
}
private void processEnabled(final Element enabled) {
- final String streamId;
+ final String id;
if (enabled.getAttributeAsBoolean("resume")) {
- streamId = enabled.getAttribute("id");
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": stream management enabled (resumable)");
+ id = enabled.getAttribute("id");
+ } else {
+ id = null;
+ }
+ final String locationAttribute = enabled.getAttribute("location");
+ final Resolver.Result currentResolverResult = this.currentResolverResult;
+ final Resolver.Result location;
+ if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
+ location = null;
+ } else {
+ location = currentResolverResult.seeOtherHost(locationAttribute);
+ }
+ final StreamId streamId = id == null ? null : new StreamId(id, location);
+ if (streamId == null) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled");
} else {
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid().toString() + ": stream management enabled");
- streamId = null;
+ account.getJid().asBareJid() + ": stream management enabled. resume at: " + streamId.location);
}
this.streamId = streamId;
this.stanzasReceived = 0;
@@ -1363,7 +1400,7 @@ public class XmppConnection implements Runnable {
account.getJid().asBareJid()
+ ": quick start in progress. ignoring features: "
+ XmlHelper.printElementNames(this.streamFeatures));
- if (SaslMechanism.hashedToken(this.saslMechanism)) {
+ if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
return;
}
if (isFastTokenAvailable(
@@ -1421,7 +1458,7 @@ public class XmppConnection implements Runnable {
+ ": resuming after stanza #"
+ stanzasReceived);
}
- final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
+ final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
this.mSmCatchupMessageCounter.set(0);
this.mWaitingForSmCatchup.set(true);
this.tagWriter.writeStanzaAsync(resume);
@@ -1448,7 +1485,8 @@ public class XmppConnection implements Runnable {
private void authenticate() throws IOException {
final boolean isSecure = isSecure();
- if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2);
+ if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
+ authenticate(SaslMechanism.Version.SASL_2);
} else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
authenticate(SaslMechanism.Version.SASL);
} else {
@@ -1473,10 +1511,10 @@ public class XmppConnection implements Runnable {
final Collection channelBindings = ChannelBinding.of(cbElement);
final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
- this.saslMechanism = validate(saslMechanism, mechanisms);
+ this.validate(saslMechanism, mechanisms);
final boolean quickStartAvailable;
- final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
- final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism);
+ final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
+ final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
final Element authenticate;
if (version == SaslMechanism.Version.SASL) {
authenticate = new Element("auth", Namespace.SASL);
@@ -1484,6 +1522,7 @@ public class XmppConnection implements Runnable {
authenticate.setContent(firstMessage);
}
quickStartAvailable = false;
+ this.loginInfo = new LoginInfo(saslMechanism,version,Collections.emptyList());
} else if (version == SaslMechanism.Version.SASL_2) {
final Element inline = authElement.findChild("inline", Namespace.SASL_2);
final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
@@ -1512,6 +1551,7 @@ public class XmppConnection implements Runnable {
return;
}
}
+ this.loginInfo = new LoginInfo(saslMechanism,version,bindFeatures);
this.hashTokenRequest = hashTokenRequest;
authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
} else {
@@ -1528,8 +1568,8 @@ public class XmppConnection implements Runnable {
+ ": Authenticating with "
+ version
+ "/"
- + this.saslMechanism.getMechanism());
- authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism());
+ + LoginInfo.mechanism(this.loginInfo).getMechanism());
+ authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism());
synchronized (this.mStanzaQueue) {
this.stanzasSentBeforeAuthentication = this.stanzasSent;
tagWriter.writeElement(authenticate);
@@ -1541,8 +1581,7 @@ public class XmppConnection implements Runnable {
return inline != null && inline.hasChild("fast", Namespace.FAST);
}
- @NonNull
- private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException {
+ private void validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException {
if (saslMechanism == null) {
Log.d(
Config.LOGTAG,
@@ -1552,7 +1591,7 @@ public class XmppConnection implements Runnable {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
if (SaslMechanism.hashedToken(saslMechanism)) {
- return saslMechanism;
+ return;
}
final int pinnedMechanism = account.getPinnedMechanismPriority();
if (pinnedMechanism > saslMechanism.getPriority()) {
@@ -1567,7 +1606,6 @@ public class XmppConnection implements Runnable {
+ "). Possible downgrade attack?");
throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
}
- return saslMechanism;
}
private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
@@ -1594,13 +1632,14 @@ public class XmppConnection implements Runnable {
.addChild("device")
.setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
}
- // do not include bind if 'inlinestreamManagment' is missing and we have a streamId
+ // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
+ // (because we would rather just do a normal SM/resume)
final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
if (bind != null && mayAttemptBind) {
authenticate.addChild(generateBindRequest(bind));
}
if (inlineStreamManagement && streamId != null) {
- final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
+ final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
this.mSmCatchupMessageCounter.set(0);
this.mWaitingForSmCatchup.set(true);
authenticate.addChild(resume);
@@ -1772,7 +1811,7 @@ public class XmppConnection implements Runnable {
synchronized (this.commands) {
this.commands.clear();
}
- this.saslMechanism = null;
+ this.loginInfo = null;
}
private void sendBindRequest() {
@@ -2268,7 +2307,7 @@ public class XmppConnection implements Runnable {
&& quickStartMechanism != null
&& account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
mXmppConnectionService.restoredFromDatabaseLatch.await();
- this.saslMechanism = quickStartMechanism;
+ this.loginInfo = new LoginInfo(quickStartMechanism, SaslMechanism.Version.SASL_2, Bind2.QUICKSTART_FEATURES);
final boolean usingFast = quickStartMechanism instanceof HashedToken;
final Element authenticate =
generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
@@ -2298,7 +2337,7 @@ public class XmppConnection implements Runnable {
}
stream.setAttribute("version", "1.0");
stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
- stream.setAttribute("xmlns", "jabber:client");
+ stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
stream.setAttribute("xmlns:stream", Namespace.STREAMS);
tagWriter.writeTag(stream, flush);
}
@@ -2681,6 +2720,49 @@ public class XmppConnection implements Runnable {
}
}
+ private static class LoginInfo {
+ public final SaslMechanism saslMechanism;
+ public final SaslMechanism.Version saslVersion;
+ public final List inlineBindFeatures;
+
+ private LoginInfo(
+ final SaslMechanism saslMechanism,
+ final SaslMechanism.Version saslVersion,
+ final Collection inlineBindFeatures) {
+ Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
+ Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
+ this.saslMechanism = saslMechanism;
+ this.saslVersion = saslVersion;
+ this.inlineBindFeatures =
+ inlineBindFeatures == null
+ ? Collections.emptyList()
+ : ImmutableList.copyOf(inlineBindFeatures);
+ }
+
+ public static SaslMechanism mechanism(final LoginInfo loginInfo) {
+ return loginInfo == null ? null : loginInfo.saslMechanism;
+ }
+ }
+
+ private static class StreamId {
+ public final String id;
+ public final Resolver.Result location;
+
+ private StreamId(String id, Resolver.Result location) {
+ this.id = id;
+ this.location = location;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("id", id)
+ .add("location", location)
+ .toString();
+ }
+ }
+
private static class StateChangingError extends Error {
private final Account.State state;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java
index 21c957a0f46e6a5c4ae526412d6dc1eb2d1b3e22..c3f847ecab4bdc9df1b3886aaba7f833c9fa34bd 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java
@@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.bind;
+import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import java.util.Arrays;
@@ -27,7 +28,12 @@ public class Bind2 {
if (inlineBind2Inline == null) {
return Collections.emptyList();
}
- return Collections2.transform(
- inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var"));
+ return Collections2.filter(
+ Collections2.transform(
+ Collections2.filter(
+ inlineBind2Inline.getChildren(),
+ c -> "feature".equals(c.getName())),
+ c -> c.getAttribute("var")),
+ Predicates.notNull());
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..847678a05753255f1bd600ef928403d0e289038e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java
@@ -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> contents;
+
+ protected AbstractContentMap(
+ final Group group, final Map> 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 getSenders() {
+ return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
+ }
+
+ public List 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> 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);
+ }
+ 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> entry : this.contents.entrySet()) {
+ if (entry.getValue().description == null) {
+ throw new IllegalStateException(
+ String.format("%s is lacking content description", entry.getKey()));
+ }
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java
index d719c729e95a62a0ac5cb5475c528d437196404b..efc32f5ff2eacf7e2ae921a9f5d5ac093d951d8b 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java
@@ -1,47 +1,352 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presence;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
public abstract class AbstractJingleConnection {
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
+ protected static final List 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> VALID_TRANSITIONS;
+
+ static {
+ final ImmutableMap.Builder> transitionBuilder =
+ new ImmutableMap.Builder<>();
+ transitionBuilder.put(
+ State.NULL,
+ ImmutableList.of(
+ State.PROPOSED,
+ State.SESSION_INITIALIZED,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.PROPOSED,
+ ImmutableList.of(
+ State.ACCEPTED,
+ State.PROCEED,
+ State.REJECTED,
+ State.RETRACTED,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR,
+ State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
+ // rebinds
+ ));
+ transitionBuilder.put(
+ State.PROCEED,
+ ImmutableList.of(
+ State.REJECTED_RACED,
+ State.RETRACTED_RACED,
+ State.SESSION_INITIALIZED_PRE_APPROVED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR,
+ State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
+ // bounces of the proceed message
+ ));
+ transitionBuilder.put(
+ State.SESSION_INITIALIZED,
+ ImmutableList.of(
+ State.SESSION_ACCEPTED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
+ // and IQ timeouts
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.SESSION_INITIALIZED_PRE_APPROVED,
+ ImmutableList.of(
+ State.SESSION_ACCEPTED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
+ // and IQ timeouts
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.SESSION_ACCEPTED,
+ ImmutableList.of(
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR,
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ VALID_TRANSITIONS = transitionBuilder.build();
+ }
+
final JingleConnectionManager jingleConnectionManager;
protected final XmppConnectionService xmppConnectionService;
protected final Id id;
private final Jid initiator;
- AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
+ protected State state = State.NULL;
+
+ AbstractJingleConnection(
+ final JingleConnectionManager jingleConnectionManager,
+ final Id id,
+ final Jid initiator) {
this.jingleConnectionManager = jingleConnectionManager;
this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
this.id = id;
this.initiator = initiator;
}
+ public Id getId() {
+ return id;
+ }
+
boolean isInitiator() {
return initiator.equals(id.account.getJid());
}
+ boolean isResponder() {
+ return !initiator.equals(id.account.getJid());
+ }
+
+ public State getState() {
+ return this.state;
+ }
+
+ protected synchronized boolean isInState(State... state) {
+ return Arrays.asList(state).contains(this.state);
+ }
+
+ protected boolean transition(final State target) {
+ return transition(target, null);
+ }
+
+ protected synchronized boolean transition(final State target, final Runnable runnable) {
+ final Collection 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);
- public Id getId() {
- return id;
+ protected void receiveOutOfOrderAction(
+ final JinglePacket jinglePacket, final JinglePacket.Action action) {
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: received %s even though we are in state %s",
+ id.account.getJid().asBareJid(), action, getState()));
+ if (isTerminated()) {
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: got a reason to terminate with out-of-order. but already in state %s",
+ id.account.getJid().asBareJid(), getState()));
+ respondWithOutOfOrder(jinglePacket);
+ } else {
+ terminateWithOutOfOrder(jinglePacket);
+ }
+ }
+
+ protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": terminating session with out-of-order");
+ terminateTransport();
+ transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
+ respondWithOutOfOrder(jinglePacket);
+ this.finish();
}
+ protected void finish() {
+ if (isTerminated()) {
+ this.jingleConnectionManager.finishConnectionOrThrow(this);
+ } else {
+ throw new AssertionError(
+ String.format("Unable to call finish from %s", this.state));
+ }
+ }
+
+ protected abstract void terminateTransport();
+
abstract void notifyRebound();
+ protected void sendSessionTerminate(
+ final Reason reason, final String text, final Consumer 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 features =
+ serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
+ return features != null && features.contains(feature);
+ }
public static class Id implements OngoingRtpSession {
public final Account account;
@@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection {
return new Id(
message.getConversation().getAccount(),
message.getCounterpart(),
- JingleConnectionManager.nextRandomId()
- );
+ JingleConnectionManager.nextRandomId());
}
public Contact getContact() {
@@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o;
- return Objects.equal(account.getUuid(), id.account.getUuid()) &&
- Objects.equal(with, id.with) &&
- Objects.equal(sessionId, id.sessionId);
+ return Objects.equal(account.getUuid(), id.account.getUuid())
+ && Objects.equal(with, id.with)
+ && Objects.equal(sessionId, id.sessionId);
}
@Override
@@ -122,23 +426,36 @@ public abstract class AbstractJingleConnection {
}
}
+ protected static State reasonToState(Reason reason) {
+ return switch (reason) {
+ case SUCCESS -> State.TERMINATED_SUCCESS;
+ case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
+ case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
+ case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
+ case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
+ .TERMINATED_APPLICATION_FAILURE;
+ default -> State.TERMINATED_CONNECTIVITY_ERROR;
+ };
+ }
public enum State {
- NULL, //default value; nothing has been sent or received yet
+ NULL, // default value; nothing has been sent or received yet
PROPOSED,
ACCEPTED,
PROCEED,
REJECTED,
- REJECTED_RACED, //used when we want to reject but haven’t received session init yet
+ REJECTED_RACED, // used when we want to reject but haven’t received session init yet
RETRACTED,
- RETRACTED_RACED, //used when receiving a retract after we already asked to proceed
- SESSION_INITIALIZED, //equal to 'PENDING'
+ RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
+ SESSION_INITIALIZED, // equal to 'PENDING'
SESSION_INITIALIZED_PRE_APPROVED,
- SESSION_ACCEPTED, //equal to 'ACTIVE'
- TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close
- TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call)
- TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button)
- TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted
+ SESSION_ACCEPTED, // equal to 'ACTIVE'
+ TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
+ TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
+ TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
+ // display retry button)
+ TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
+ // before session was accepted
TERMINATED_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java
index 97bf802fd7fea0b1a97485fb640f7af3aa6fa7e8..ab2dffc6ddb210d2a51440629cbddd06ed719000 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import androidx.annotation.NonNull;
+
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.Collections2;
@@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet;
import java.util.Set;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public final class ContentAddition {
@@ -32,12 +36,13 @@ public final class ContentAddition {
Collections2.transform(
rtpContentMap.contents.entrySet(),
e -> {
- final RtpContentMap.DescriptionTransport dt = e.getValue();
+ final DescriptionTransport dt = e.getValue();
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
}));
}
@Override
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("direction", direction)
@@ -77,6 +82,7 @@ public final class ContentAddition {
}
@Override
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java
new file mode 100644
index 0000000000000000000000000000000000000000..70d6c512c786211dbd65e8e09e65167ed82da395
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java
@@ -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 {
+
+ 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;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java
index 83a2b95e4cf98398a5bd1ad29f75cfa51f4f565c..a2a5c40327950259e5a182fa0eabbdd1258531c0 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import com.google.common.collect.ImmutableList;
+
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
@@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid;
public class DirectConnectionUtils {
- private static List getLocalAddresses() {
- final List addresses = new ArrayList<>();
+ public static List getLocalAddresses() {
+ final ImmutableList.Builder inetAddresses = new ImmutableList.Builder<>();
final Enumeration interfaces;
try {
interfaces = NetworkInterface.getNetworkInterfaces();
- } catch (SocketException e) {
- return addresses;
+ } catch (final SocketException e) {
+ return inetAddresses.build();
}
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
@@ -34,31 +36,15 @@ public class DirectConnectionUtils {
if (inetAddress instanceof Inet6Address) {
//let's get rid of scope
try {
- addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
+ inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
} catch (UnknownHostException e) {
//ignored
}
} else {
- addresses.add(inetAddress);
+ inetAddresses.add(inetAddress);
}
}
}
- return addresses;
+ return inetAddresses.build();
}
-
- public static List getLocalCandidates(Jid jid) {
- SecureRandom random = new SecureRandom();
- ArrayList 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;
- }
-
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..c678c91cb8b1de6fba4994a69ec3408620202954
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java
@@ -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 {
+
+ private static final List> SUPPORTED_TRANSPORTS =
+ Arrays.asList(
+ SocksByteStreamsTransportInfo.class,
+ IbbTransportInfo.class,
+ WebRTCDataChannelTransportInfo.class);
+
+ protected FileTransferContentMap(
+ final Group group, final Map>
+ contents) {
+ super(group, contents);
+ }
+
+ public static FileTransferContentMap of(final JinglePacket jinglePacket) {
+ final Map>
+ contents = of(jinglePacket.getJingleContents());
+ return new FileTransferContentMap(jinglePacket.getGroup(), contents);
+ }
+
+ public static DescriptionTransport 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>
+ of(final Map 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 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)));
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b2f884578f55ed00e28db715b23db1ac534ba7a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
@@ -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 parse(final IqPacket response) {
+ ImmutableList.Builder listBuilder = new ImmutableList.Builder<>();
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ final Element services =
+ response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
+ final List 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();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
deleted file mode 100644
index 78ffb28bee9a72a79a1bcc9fa0b224aab1ddfb76..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
+++ /dev/null
@@ -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 parse(final List elements) {
- final List 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());
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
index 48f011f098877b225fdc6fe08528d6b825cb415f..026f06ad559b9b08508e5e012b37b0417b017d3a 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -12,20 +12,6 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableSet;
-import java.lang.ref.WeakReference;
-import java.security.SecureRandom;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@@ -39,18 +25,33 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.jingle.stanzas.Propose;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import java.lang.ref.WeakReference;
+import java.security.SecureRandom;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
public class JingleConnectionManager extends AbstractConnectionManager {
static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
Executors.newSingleThreadScheduledExecutor();
@@ -63,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private final Cache terminatedSessions =
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
- private final HashMap primaryCandidates = new HashMap<>();
-
public JingleConnectionManager(XmppConnectionService service) {
super(service);
this.toneManager = new ToneManager(service);
@@ -92,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final String descriptionNamespace =
content == null ? null : content.getDescriptionNamespace();
final AbstractJingleConnection connection;
- if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) {
connection = new JingleFileTransferConnection(this, id, from);
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
&& isUsingClearNet(account)) {
@@ -170,8 +169,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public boolean hasJingleRtpConnection(final Account account) {
for (AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
@@ -185,8 +183,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public void notifyPhoneCallStarted() {
for (AbstractJingleConnection connection : connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
@@ -195,7 +192,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
-
private Optional findMatchingSessionProposal(
final Account account, final Jid with, final Set media) {
synchronized (this.rtpSessionProposals) {
@@ -220,8 +216,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private String hasMatchingRtpSession(final Account account, final Jid with, final Set media) {
for (AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
@@ -281,8 +276,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
if ("accept".equals(message.getName()) || "reject".equals(message.getName())) {
for (AbstractJingleConnection connection : connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
final AbstractJingleConnection.Id id = connection.getId();
if (id.account == account && id.sessionId.equals(sessionId)) {
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
@@ -599,13 +593,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (old != null) {
old.cancel();
}
- final Account account = message.getConversation().getAccount();
- final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message);
final JingleFileTransferConnection connection =
- new JingleFileTransferConnection(this, id, account.getJid());
- mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
- this.connections.put(id, connection);
- connection.init(message);
+ new JingleFileTransferConnection(this, message);
+ this.connections.put(connection.getId(), connection);
+ connection.sendSessionInitialize();
}
public Optional getOngoingRtpConnection(final Contact contact) {
@@ -644,15 +635,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final AbstractJingleConnection.Id id = connection.getId();
if (this.connections.remove(id) == null) {
throw new IllegalStateException(
- String.format("Unable to finish connection with id=%s", id.toString()));
+ String.format("Unable to finish connection with id=%s", id));
}
+ // update chat UI to remove 'ongoing call' icon
+ mXmppConnectionService.updateConversationUi();
}
public boolean fireJingleRtpConnectionStateUpdates() {
boolean firedUpdates = false;
for (final AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection jingleRtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection jingleRtpConnection) {
if (jingleRtpConnection.isTerminated()) {
continue;
}
@@ -663,73 +655,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return firedUpdates;
}
- void getPrimaryCandidate(
- final Account account,
- final boolean initiator,
- final OnPrimaryCandidateFound listener) {
- if (Config.DISABLE_PROXY_LOOKUP) {
- listener.onPrimaryCandidateFound(false, null);
- return;
- }
- if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
- final Jid proxy =
- account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
- if (proxy != null) {
- IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
- iq.setTo(proxy);
- iq.query(Namespace.BYTE_STREAMS);
- account.getXmppConnection()
- .sendIqPacket(
- iq,
- new OnIqPacketReceived() {
-
- @Override
- public void onIqPacketReceived(
- Account account, IqPacket packet) {
- final Element streamhost =
- packet.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(
- account.getJid().asBareJid(), candidate);
- listener.onPrimaryCandidateFound(true, candidate);
- } catch (final NumberFormatException e) {
- listener.onPrimaryCandidateFound(false, null);
- }
- } else {
- listener.onPrimaryCandidateFound(false, null);
- }
- }
- });
- } else {
- listener.onPrimaryCandidateFound(false, null);
- }
-
- } else {
- listener.onPrimaryCandidateFound(
- true, this.primaryCandidates.get(account.getJid().asBareJid()));
- }
- }
-
public void retractSessionProposal(final Account account, final Jid with) {
synchronized (this.rtpSessionProposals) {
RtpSessionProposal matchingProposal = null;
@@ -831,40 +756,53 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return false;
}
- public void deliverIbbPacket(Account account, IqPacket packet) {
+ public void deliverIbbPacket(final Account account, final IqPacket packet) {
final String sid;
final Element payload;
+ final InbandBytestreamsTransport.PacketType packetType;
if (packet.hasChild("open", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.OPEN;
payload = packet.findChild("open", Namespace.IBB);
sid = payload.getAttribute("sid");
} else if (packet.hasChild("data", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.DATA;
payload = packet.findChild("data", Namespace.IBB);
sid = payload.getAttribute("sid");
} else if (packet.hasChild("close", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.CLOSE;
payload = packet.findChild("close", Namespace.IBB);
sid = payload.getAttribute("sid");
} else {
+ packetType = null;
payload = null;
sid = null;
}
- if (sid != null) {
- for (final AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleFileTransferConnection) {
- final JingleFileTransferConnection fileTransfer =
- (JingleFileTransferConnection) connection;
- final JingleTransport transport = fileTransfer.getTransport();
- if (transport instanceof JingleInBandTransport) {
- final JingleInBandTransport inBandTransport =
- (JingleInBandTransport) transport;
- if (inBandTransport.matches(account, sid)) {
- inBandTransport.deliverPayload(packet, payload);
+ if (sid == null) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid");
+ account.getXmppConnection()
+ .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
+ return;
+ }
+ for (final AbstractJingleConnection connection : this.connections.values()) {
+ if (connection instanceof JingleFileTransferConnection fileTransfer) {
+ final Transport transport = fileTransfer.getTransport();
+ if (transport instanceof InbandBytestreamsTransport inBandTransport) {
+ if (sid.equals(inBandTransport.getStreamId())) {
+ if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) {
+ account.getXmppConnection()
+ .sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ account.getXmppConnection()
+ .sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
return;
}
}
}
}
- Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
+ Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid);
account.getXmppConnection()
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
@@ -970,7 +908,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
- public void failProceed(Account account, final Jid with, final String sessionId, final String message) {
+ public void failProceed(
+ Account account, final Jid with, final String sessionId, final String message) {
final AbstractJingleConnection.Id id =
AbstractJingleConnection.Id.of(account, with, sessionId);
final AbstractJingleConnection existingJingleConnection = connections.get(id);
@@ -1044,15 +983,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
FAILED;
public RtpEndUserState toEndUserState() {
- switch (this) {
- case SEARCHING:
- case SEARCHING_ACKNOWLEDGED:
- return RtpEndUserState.FINDING_DEVICE;
- case DISCOVERED:
- return RtpEndUserState.RINGING;
- default:
- return RtpEndUserState.CONNECTIVITY_ERROR;
- }
+ return switch (this) {
+ case SEARCHING, SEARCHING_ACKNOWLEDGED -> RtpEndUserState.FINDING_DEVICE;
+ case DISCOVERED -> RtpEndUserState.RINGING;
+ default -> RtpEndUserState.CONNECTIVITY_ERROR;
+ };
}
}
@@ -1062,10 +997,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public final Set media;
private final Account account;
- private RtpSessionProposal(Account account, Jid with, String sessionId) {
- this(account, with, sessionId, Collections.emptySet());
- }
-
private RtpSessionProposal(Account account, Jid with, String sessionId, Set media) {
this.account = account;
this.with = with;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
index 1b21966916e0e38aef825f768c32e7a2c6be08a7..983ce433f610f977d5df317560b621cd0829623e 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
@@ -1,1279 +1,1454 @@
package eu.siacs.conversations.xmpp.jingle;
-import android.util.Base64;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
+import com.google.common.hash.Hashing;
+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.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
-import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
-import eu.siacs.conversations.parser.IqParser;
-import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.MimeUtils;
-import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
-import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
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.Reason;
-import eu.siacs.conversations.xmpp.jingle.stanzas.S5BTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable {
-
- private static final int JINGLE_STATUS_TRANSMITTING = 5;
- private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
- private static final int JINGLE_STATUS_INITIATED = 0;
- private static final int JINGLE_STATUS_ACCEPTED = 1;
- private static final int JINGLE_STATUS_FINISHED = 4;
- private static final int JINGLE_STATUS_FAILED = 99;
- private static final int JINGLE_STATUS_OFFERED = -1;
-
- private static final int MAX_IBB_BLOCK_SIZE = 8192;
-
- private int ibbBlockSize = MAX_IBB_BLOCK_SIZE;
-
- private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum
- private int mStatus = Transferable.STATUS_UNKNOWN;
- private Message message;
- private Jid responder;
- private final List candidates = new ArrayList<>();
- private final ConcurrentHashMap connections = new ConcurrentHashMap<>();
-
- private String transportId;
- private FileTransferDescription description;
- private DownloadableFile file = null;
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherInputStream;
+import org.bouncycastle.crypto.io.CipherOutputStream;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.webrtc.IceCandidate;
+
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
- private boolean proxyActivationFailed = false;
+public class JingleFileTransferConnection extends AbstractJingleConnection
+ implements Transport.Callback, Transferable {
- private String contentName;
- private Content.Creator contentCreator;
- private Content.Senders contentSenders;
- private Class extends GenericTransportInfo> initialTransport;
- private boolean remoteSupportsOmemoJet;
+ private final Message message;
- private int mProgress = 0;
+ private FileTransferContentMap initiatorFileTransferContentMap;
+ private FileTransferContentMap responderFileTransferContentMap;
- private boolean receivedCandidate = false;
- private boolean sentCandidate = false;
+ private Transport transport;
+ private TransportSecurity transportSecurity;
+ private AbstractFileTransceiver fileTransceiver;
+ private final Queue pendingIncomingIceCandidates = new LinkedList<>();
private boolean acceptedAutomatically = false;
- private boolean cancelled = false;
-
- private XmppAxolotlMessage mXmppAxolotlMessage;
- private JingleTransport transport = null;
+ public JingleFileTransferConnection(
+ final JingleConnectionManager jingleConnectionManager, final Message message) {
+ super(
+ jingleConnectionManager,
+ AbstractJingleConnection.Id.of(message),
+ message.getConversation().getAccount().getJid());
+ Preconditions.checkArgument(
+ message.isFileOrImage(),
+ "only file or images messages can be transported via jingle");
+ this.message = message;
+ this.message.setTransferable(this);
+ xmppConnectionService.markMessage(message, Message.STATUS_WAITING);
+ }
- private OutputStream mFileOutputStream;
- private InputStream mFileInputStream;
+ public JingleFileTransferConnection(
+ final JingleConnectionManager jingleConnectionManager,
+ final Id id,
+ final Jid initiator) {
+ super(jingleConnectionManager, id, initiator);
+ final Conversation conversation =
+ this.xmppConnectionService.findOrCreateConversation(
+ id.account, id.with.asBareJid(), false, false);
+ this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
+ this.message.setStatus(Message.STATUS_RECEIVED);
+ this.message.setErrorMessage(null);
+ this.message.setTransferable(this);
+ }
- private final OnIqPacketReceived responseListener = (account, packet) -> {
- if (packet.getType() != IqPacket.TYPE.RESULT) {
- if (mJingleStatus != JINGLE_STATUS_FAILED && mJingleStatus != JINGLE_STATUS_FINISHED) {
- fail(IqParser.extractErrorMessage(packet));
- } else {
- Log.d(Config.LOGTAG, "ignoring late delivery of jingle packet to jingle session with status=" + mJingleStatus + ": " + packet.toString());
+ @Override
+ void deliverPacket(final JinglePacket jinglePacket) {
+ switch (jinglePacket.getAction()) {
+ case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
+ case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
+ case SESSION_INFO -> receiveSessionInfo(jinglePacket);
+ case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
+ case TRANSPORT_ACCEPT -> receiveTransportAccept(jinglePacket);
+ case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
+ case TRANSPORT_REPLACE -> receiveTransportReplace(jinglePacket);
+ default -> {
+ respondOk(jinglePacket);
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: received unhandled jingle action %s",
+ id.account.getJid().asBareJid(), jinglePacket.getAction()));
}
}
- };
- private byte[] expectedHash = new byte[0];
- private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
+ }
- @Override
- public void onFileTransmitted(DownloadableFile file) {
- DownloadableFile finalFile;
-
- if (responding()) {
- if (expectedHash.length > 0) {
- if (Arrays.equals(expectedHash, file.getSha1Sum())) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received file matched the expected hash");
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match");
+ public void sendSessionInitialize() {
+ final ListenableFuture> keyTransportMessage;
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ keyTransportMessage =
+ Futures.transform(
+ id.account
+ .getAxolotlService()
+ .prepareKeyTransportMessage(requireConversation()),
+ Optional::of,
+ MoreExecutors.directExecutor());
+ } else {
+ keyTransportMessage = Futures.immediateFuture(Optional.empty());
+ }
+ Futures.addCallback(
+ keyTransportMessage,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Optional xmppAxolotlMessage) {
+ sendSessionInitialize(xmppAxolotlMessage.orElse(null));
}
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer");
- }
- sendSuccess();
-
- final String extension = MimeUtils.extractRelevantExtension(file.getName());
- try {
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message, new FileInputStream(file), extension);
- finalFile = xmppConnectionService.getFileBackend().getFile(message);
- boolean didRename = file.renameTo(finalFile);
- if (!didRename) throw new IOException("rename failed");
- } catch (final IOException e) {
- finalFile = file;
- message.setRelativeFilePath(finalFile.getAbsolutePath());
- } catch (final XmppConnectionService.BlockedMediaException e) {
- finalFile = file;
- file.delete();
- message.setRelativeFilePath(null);
- message.setDeleted(true);
- }
-
- xmppConnectionService.getFileBackend().updateFileParams(message, null, false);
- xmppConnectionService.databaseBackend.createMessage(message);
- xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
- if (acceptedAutomatically) {
- message.markUnread();
- if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, true);
- } else {
- xmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleFileTransferConnection.this.xmppConnectionService.getNotificationService().push(message));
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "can not send message");
}
- Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
- return;
- } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, true);
- }
- } else {
- finalFile = file;
-
- if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info
- sendHash();
- }
- if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, false);
- }
- if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- finalFile.delete();
- }
- disconnectSocks5Connections();
- }
- Log.d(Config.LOGTAG, "successfully transmitted file:" + finalFile.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
- if (message.getEncryption() != Message.ENCRYPTION_PGP) {
- xmppConnectionService.getFileBackend().updateMediaScanner(finalFile);
- }
- }
+ },
+ MoreExecutors.directExecutor());
+ }
- @Override
- public void onFileTransferAborted() {
- JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
- JingleFileTransferConnection.this.fail();
- }
- };
- private final OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, "ibb open failed");
- }
+ private void sendSessionInitialize(final XmppAxolotlMessage xmppAxolotlMessage) {
+ this.transport = setupTransport();
+ this.transport.setTransportCallback(this);
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ final var fileDescription =
+ new FileTransferDescription.File(
+ file.length(),
+ file.getName(),
+ message.getMimeType(),
+ Collections.emptyList());
+ final var transportInfoFuture = this.transport.asInitialTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(
+ final Transport.InitialTransportInfo initialTransportInfo) {
+ final FileTransferContentMap contentMap =
+ FileTransferContentMap.of(fileDescription, initialTransportInfo);
+ sendSessionInitialize(xmppAxolotlMessage, contentMap);
+ }
- @Override
- public void established() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file");
- mJingleStatus = JINGLE_STATUS_TRANSMITTING;
- JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged);
- }
- };
- private final OnProxyActivated onProxyActivated = new OnProxyActivated() {
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {}
+ },
+ MoreExecutors.directExecutor());
+ }
- @Override
- public void success() {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, "we were initiating. sending file");
- transport.send(file, onFileTransmissionStatusChanged);
- } else {
- transport.receive(file, onFileTransmissionStatusChanged);
- Log.d(Config.LOGTAG, "we were responding. receiving file");
- }
+ private Conversation requireConversation() {
+ final var conversational = message.getConversation();
+ if (conversational instanceof Conversation c) {
+ return c;
+ } else {
+ throw new IllegalStateException("Message had no proper conversation attached");
}
+ }
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed");
- proxyActivationFailed = true;
- if (isInitiator()) {
- sendFallbackToIbb();
+ private void sendSessionInitialize(
+ final XmppAxolotlMessage xmppAxolotlMessage, final FileTransferContentMap contentMap) {
+ if (transition(
+ State.SESSION_INITIALIZED,
+ () -> this.initiatorFileTransferContentMap = contentMap)) {
+ final var jinglePacket =
+ contentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+ if (xmppAxolotlMessage != null) {
+ this.transportSecurity =
+ new TransportSecurity(
+ xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV());
+ jinglePacket.setSecurity(
+ Iterables.getOnlyElement(contentMap.contents.keySet()), xmppAxolotlMessage);
}
+ Log.d(Config.LOGTAG, "--> " + jinglePacket.toString());
+ jinglePacket.setTo(id.with);
+ xmppConnectionService.sendIqPacket(
+ id.account,
+ jinglePacket,
+ (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.ERROR) {
+ handleIqErrorResponse(response);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ handleIqTimeoutResponse(response);
+ }
+ });
+ this.transport.readyToSentAdditionalCandidates();
}
- };
-
- JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
- super(jingleConnectionManager, id, initiator);
}
- private static long parseLong(final Element element, final long l) {
- final String input = element == null ? null : element.getContent();
- if (input == null) {
- return l;
+ private void receiveSessionAccept(final JinglePacket jinglePacket) {
+ Log.d(Config.LOGTAG, "receive session accept " + jinglePacket);
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+ return;
}
+ final FileTransferContentMap contentMap;
try {
- return Long.parseLong(input);
- } catch (Exception e) {
- return l;
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ contentMap.requireOnlyFileTransferDescription();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
}
+ receiveSessionAccept(jinglePacket, contentMap);
}
- //TODO get rid and use isInitiator() instead
- private boolean responding() {
- return responder != null && responder.equals(id.account.getJid());
+ private void receiveSessionAccept(
+ final JinglePacket jinglePacket, final FileTransferContentMap contentMap) {
+ if (transition(State.SESSION_ACCEPTED, () -> setRemoteContentMap(contentMap))) {
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (configureTransportWithPeerInfo(transport, contentMap)) {
+ transport.connect();
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ "Transport in session accept did not match our session-initialize");
+ terminateTransport();
+ sendSessionTerminate(
+ Reason.FAILED_APPLICATION,
+ "Transport in session accept did not match our session-initialize");
+ }
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": receive out of order session-accept");
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+ }
}
-
- InputStream getFileInputStream() {
- return this.mFileInputStream;
+ private static boolean configureTransportWithPeerInfo(
+ final Transport transport, final FileTransferContentMap contentMap) {
+ final GenericTransportInfo transportInfo = contentMap.requireOnlyTransportInfo();
+ if (transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport
+ && transportInfo instanceof WebRTCDataChannelTransportInfo) {
+ webRTCDataChannelTransport.setResponderDescription(SessionDescription.of(contentMap));
+ return true;
+ } else if (transport instanceof SocksByteStreamsTransport socksBytestreamsTransport
+ && transportInfo
+ instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ socksBytestreamsTransport.setTheirCandidates(
+ socksBytestreamsTransportInfo.getCandidates());
+ return true;
+ } else if (transport instanceof InbandBytestreamsTransport inbandBytestreamsTransport
+ && transportInfo instanceof IbbTransportInfo ibbTransportInfo) {
+ final var peerBlockSize = ibbTransportInfo.getBlockSize();
+ if (peerBlockSize != null) {
+ inbandBytestreamsTransport.setPeerBlockSize(peerBlockSize);
+ }
+ return true;
+ } else {
+ return false;
+ }
}
- OutputStream getFileOutputStream() throws IOException {
- if (this.file == null) {
- Log.d(Config.LOGTAG, "file object was not assigned");
- return null;
+ private void receiveSessionInitiate(final JinglePacket jinglePacket) {
+ if (isInitiator()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
+ return;
}
- final File parent = this.file.getParentFile();
- if (parent != null && parent.mkdirs()) {
- Log.d(Config.LOGTAG, "created parent directories for file " + file.getAbsolutePath());
+ Log.d(Config.LOGTAG, "receive session initiate " + jinglePacket);
+ final FileTransferContentMap contentMap;
+ final FileTransferDescription.File file;
+ try {
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ contentMap.requireContentDescriptions();
+ file = contentMap.requireOnlyFile();
+ // TODO check is offer
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
}
- if (this.file.createNewFile()) {
- Log.d(Config.LOGTAG, "created output file " + file.getAbsolutePath());
+ final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
+ final var security =
+ jinglePacket.getSecurity(Iterables.getOnlyElement(contentMap.contents.keySet()));
+ if (security != null) {
+ Log.d(Config.LOGTAG, "found security element!");
+ keyTransportMessage =
+ id.account
+ .getAxolotlService()
+ .processReceivingKeyTransportMessage(security, false);
+ } else {
+ keyTransportMessage = null;
}
- this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true);
- return this.mFileOutputStream;
+ receiveSessionInitiate(jinglePacket, contentMap, file, keyTransportMessage);
}
- @Override
- void deliverPacket(final JinglePacket packet) {
- final JinglePacket.Action action = packet.getAction();
- //TODO switch case
- if (action == JinglePacket.Action.SESSION_INITIATE) {
- init(packet);
- } else if (action == JinglePacket.Action.SESSION_TERMINATE) {
- final Reason reason = packet.getReason().reason;
- switch (reason) {
- case CANCEL:
- this.cancelled = true;
- this.fail();
- break;
- case SUCCESS:
- this.receiveSuccess();
- break;
- default:
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate with reason " + reason);
- this.fail();
- break;
-
- }
- } else if (action == JinglePacket.Action.SESSION_ACCEPT) {
- receiveAccept(packet);
- } else if (action == JinglePacket.Action.SESSION_INFO) {
- final Element checksum = packet.getJingleChild("checksum");
- final Element file = checksum == null ? null : checksum.findChild("file");
- final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2");
- if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) {
- try {
- this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
- } catch (Exception e) {
- this.expectedHash = new byte[0];
- }
+ private void receiveSessionInitiate(
+ final JinglePacket jinglePacket,
+ final FileTransferContentMap contentMap,
+ final FileTransferDescription.File file,
+ final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage) {
+
+ if (transition(State.SESSION_INITIALIZED, () -> setRemoteContentMap(contentMap))) {
+ respondOk(jinglePacket);
+ Log.d(Config.LOGTAG, jinglePacket.toString());
+ Log.d(
+ Config.LOGTAG,
+ "got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage));
+ setFileOffer(file);
+ if (keyTransportMessage != null) {
+ this.transportSecurity =
+ new TransportSecurity(
+ keyTransportMessage.getKey(), keyTransportMessage.getIv());
+ this.message.setFingerprint(keyTransportMessage.getFingerprint());
+ this.message.setEncryption(Message.ENCRYPTION_AXOLOTL);
+ } else {
+ this.transportSecurity = null;
+ this.message.setFingerprint(null);
}
- respondToIq(packet, true);
- } else if (action == JinglePacket.Action.TRANSPORT_INFO) {
- receiveTransportInfo(packet);
- } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) {
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content == null ? null : content.getTransport();
- if (transportInfo instanceof IbbTransportInfo) {
- receiveFallbackToIbb(packet, (IbbTransportInfo) transportInfo);
+ final var conversation = (Conversation) message.getConversation();
+ conversation.add(message);
+
+ // make auto accept decision
+ if (id.account.getRoster().getContact(id.with).showInContactList()
+ && jingleConnectionManager.hasStoragePermission()
+ && file.size <= this.jingleConnectionManager.getAutoAcceptFileSize()
+ && xmppConnectionService.isDataSaverDisabled()) {
+ Log.d(Config.LOGTAG, "auto accepting file from " + id.with);
+ this.acceptedAutomatically = true;
+ this.sendSessionAccept();
} else {
- Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString());
- respondToIq(packet, false);
+ Log.d(
+ Config.LOGTAG,
+ "not auto accepting new file offer with size: "
+ + file.size
+ + " allowed size:"
+ + this.jingleConnectionManager.getAutoAcceptFileSize());
+ message.markUnread();
+ this.xmppConnectionService.updateConversationUi();
+ this.xmppConnectionService.getNotificationService().push(message);
}
- } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) {
- receiveTransportAccept(packet);
} else {
- Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction());
- respondToIq(packet, false);
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": receive out of order session-initiate");
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
}
}
- @Override
- void notifyRebound() {
- if (getJingleStatus() == JINGLE_STATUS_TRANSMITTING) {
- abort(Reason.CONNECTIVITY_ERROR);
+ private void setFileOffer(final FileTransferDescription.File file) {
+ final AbstractConnectionManager.Extension extension =
+ AbstractConnectionManager.Extension.of(file.name);
+ if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
+ this.message.setEncryption(Message.ENCRYPTION_PGP);
+ } else {
+ this.message.setEncryption(Message.ENCRYPTION_NONE);
}
+ final String ext = extension.getExtension();
+ final String filename =
+ Strings.isNullOrEmpty(ext)
+ ? message.getUuid()
+ : String.format("%s.%s", message.getUuid(), ext);
+ xmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename);
}
- private void respondToIq(final IqPacket packet, final boolean result) {
- final IqPacket response;
- if (result) {
- response = packet.generateResponse(IqPacket.TYPE.RESULT);
- } else {
- response = packet.generateResponse(IqPacket.TYPE.ERROR);
- final Element error = response.addChild("error").setAttribute("type", "cancel");
- error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
+ public void sendSessionAccept() {
+ final FileTransferContentMap contentMap = this.initiatorFileTransferContentMap;
+ final Transport transport;
+ try {
+ transport = setupTransport(contentMap.requireOnlyTransportInfo());
+ } catch (final RuntimeException e) {
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
}
- xmppConnectionService.sendIqPacket(id.account, response, null);
- }
+ transitionOrThrow(State.SESSION_ACCEPTED);
+ this.transport = transport;
+ this.transport.setTransportCallback(this);
+ if (this.transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport) {
+ final var sessionDescription = SessionDescription.of(contentMap);
+ webRTCDataChannelTransport.setInitiatorDescription(sessionDescription);
+ }
+ final var transportInfoFuture = transport.asTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Transport.TransportInfo transportInfo) {
+ final FileTransferContentMap responderContentMap =
+ contentMap.withTransport(transportInfo);
+ sendSessionAccept(responderContentMap);
+ }
- private void respondToIqWithOutOfOrder(final IqPacket packet) {
- final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
- final Element error = response.addChild("error").setAttribute("type", "wait");
- error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas");
- error.addChild("out-of-order", "urn:xmpp:jingle:errors:1");
- xmppConnectionService.sendIqPacket(id.account, response, null);
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ failureToAcceptSession(throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
}
- public void init(final Message message) {
- Preconditions.checkArgument(message.isFileOrImage());
- if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
- Conversation conversation = (Conversation) message.getConversation();
- conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> {
- if (xmppAxolotlMessage != null) {
- init(message, xmppAxolotlMessage);
- } else {
- fail();
- }
- });
- } else {
- init(message, null);
+ private void sendSessionAccept(final FileTransferContentMap contentMap) {
+ setLocalContentMap(contentMap);
+ final var jinglePacket =
+ contentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId);
+ Log.d(Config.LOGTAG, "--> " + jinglePacket.toString());
+ send(jinglePacket);
+ // this needs to come after session-accept or else our candidate-error might arrive first
+ this.transport.connect();
+ this.transport.readyToSentAdditionalCandidates();
+ if (this.transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport) {
+ drainPendingIncomingIceCandidates(webRTCDataChannelTransport);
}
}
- private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) {
- this.mXmppAxolotlMessage = xmppAxolotlMessage;
- this.contentCreator = Content.Creator.INITIATOR;
- this.contentSenders = Content.Senders.INITIATOR;
- this.contentName = JingleConnectionManager.nextRandomId();
- this.message = message;
- final List remoteFeatures = getRemoteFeatures();
- final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures);
- this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? S5BTransportInfo.class : IbbTransportInfo.class;
- this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO);
- this.message.setTransferable(this);
- this.mStatus = Transferable.STATUS_UPLOADING;
- this.responder = this.id.with;
- this.transportId = JingleConnectionManager.nextRandomId();
- this.setupDescription(remoteVersion);
- if (this.initialTransport == IbbTransportInfo.class) {
- this.sendInitRequest();
- } else {
- gatherAndConnectDirectCandidates();
- this.jingleConnectionManager.getPrimaryCandidate(id.account, isInitiator(), (success, candidate) -> {
- if (success) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort()));
- sendInitRequest();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate");
- mergeCandidate(candidate);
- sendInitRequest();
- }
- });
- mergeCandidate(candidate);
- } else {
- Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found");
- sendInitRequest();
- }
- });
+ private void drainPendingIncomingIceCandidates(
+ final WebRTCDataChannelTransport webRTCDataChannelTransport) {
+ while (this.pendingIncomingIceCandidates.peek() != null) {
+ final var candidate = this.pendingIncomingIceCandidates.poll();
+ if (candidate == null) {
+ continue;
+ }
+ webRTCDataChannelTransport.addIceCandidates(ImmutableList.of(candidate));
}
-
}
- private void gatherAndConnectDirectCandidates() {
- final List directCandidates;
- if (Config.USE_DIRECT_JINGLE_CANDIDATES) {
- if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) {
- directCandidates = Collections.emptyList();
- } else {
- directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid());
+ private Transport setupTransport(final GenericTransportInfo transportInfo) {
+ final XmppConnection xmppConnection = id.account.getXmppConnection();
+ final boolean useTor = id.account.isOnion() || xmppConnectionService.useTorToConnect();
+ if (transportInfo instanceof IbbTransportInfo ibbTransportInfo) {
+ final String streamId = ibbTransportInfo.getTransportId();
+ final Long blockSize = ibbTransportInfo.getBlockSize();
+ if (streamId == null || blockSize == null) {
+ throw new IllegalStateException("ibb transport is missing sid and/or block-size");
}
+ return new InbandBytestreamsTransport(
+ xmppConnection,
+ id.with,
+ isInitiator(),
+ streamId,
+ Ints.saturatedCast(blockSize));
+ } else if (transportInfo
+ instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ final String streamId = socksBytestreamsTransportInfo.getTransportId();
+ final String destination = socksBytestreamsTransportInfo.getDestinationAddress();
+ final List candidates =
+ socksBytestreamsTransportInfo.getCandidates();
+ Log.d(Config.LOGTAG, "received socks candidates " + candidates);
+ return new SocksByteStreamsTransport(
+ xmppConnection, id, isInitiator(), useTor, streamId, candidates);
+ } else if (!useTor && transportInfo instanceof WebRTCDataChannelTransportInfo) {
+ return new WebRTCDataChannelTransport(
+ xmppConnectionService.getApplicationContext(),
+ xmppConnection,
+ id.account,
+ isInitiator());
} else {
- directCandidates = Collections.emptyList();
- }
- for (JingleCandidate directCandidate : directCandidates) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
- connections.put(directCandidate.getCid(), socksConnection);
- candidates.add(directCandidate);
+ throw new IllegalArgumentException("Do not know how to create transport");
}
}
- private FileTransferDescription.Version getAvailableFileTransferVersion(List remoteFeatures) {
- if (remoteFeatures.contains(FileTransferDescription.Version.FT_5.getNamespace())) {
- return FileTransferDescription.Version.FT_5;
- } else if (remoteFeatures.contains(FileTransferDescription.Version.FT_4.getNamespace())) {
- return FileTransferDescription.Version.FT_4;
- } else {
- return FileTransferDescription.Version.FT_3;
+ private Transport setupTransport() {
+ final XmppConnection xmppConnection = id.account.getXmppConnection();
+ final boolean useTor = id.account.isOnion() || xmppConnectionService.useTorToConnect();
+ if (!useTor && remoteHasFeature(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL)) {
+ return new WebRTCDataChannelTransport(
+ xmppConnectionService.getApplicationContext(),
+ xmppConnection,
+ id.account,
+ isInitiator());
}
+ if (remoteHasFeature(Namespace.JINGLE_TRANSPORTS_S5B)) {
+ return new SocksByteStreamsTransport(xmppConnection, id, isInitiator(), useTor);
+ }
+ return setupLastResortTransport();
}
- private List getRemoteFeatures() {
- final String resource = Strings.nullToEmpty(this.id.with.getResource());
- final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource);
- final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
- return result == null ? Collections.emptyList() : result.getFeatures();
+ private Transport setupLastResortTransport() {
+ final XmppConnection xmppConnection = id.account.getXmppConnection();
+ return new InbandBytestreamsTransport(xmppConnection, id.with, isInitiator());
}
- private void init(JinglePacket packet) { //should move to deliverPacket
- //TODO if not 'OFFERED' reply with out-of-order
- this.mJingleStatus = JINGLE_STATUS_INITIATED;
- final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.with.asBareJid(), false, false);
- this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
- this.message.setStatus(Message.STATUS_RECEIVED);
- this.mStatus = Transferable.STATUS_OFFER;
- this.message.setTransferable(this);
- this.message.setCounterpart(this.id.with);
- this.responder = this.id.account.getJid();
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- this.contentCreator = content.getCreator();
- Content.Senders senders;
- try {
- senders = content.getSenders();
- } catch (final Exception e) {
- senders = Content.Senders.INITIATOR;
- }
- this.contentSenders = senders;
- this.contentName = content.getAttribute("name");
-
- if (transportInfo instanceof S5BTransportInfo) {
- final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
- this.transportId = s5BTransportInfo.getTransportId();
- this.initialTransport = s5BTransportInfo.getClass();
- this.mergeCandidates(s5BTransportInfo.getCandidates());
- } else if (transportInfo instanceof IbbTransportInfo) {
- final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
- this.initialTransport = ibbTransportInfo.getClass();
- this.transportId = ibbTransportInfo.getTransportId();
- final int remoteBlockSize = ibbTransportInfo.getBlockSize();
- if (remoteBlockSize <= 0) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party requested invalid ibb block size");
- respondToIq(packet, false);
- this.fail();
- }
- this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, ibbTransportInfo.getBlockSize());
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote tried to use unknown transport " + transportInfo.getNamespace());
- respondToIq(packet, false);
- this.fail();
+ private void failureToAcceptSession(final Throwable throwable) {
+ if (isTerminated()) {
return;
}
+ final Throwable rootCause = Throwables.getRootCause(throwable);
+ Log.d(Config.LOGTAG, "unable to send session accept", rootCause);
+ sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage());
+ }
- this.description = (FileTransferDescription) content.getDescription();
+ private void receiveSessionInfo(final JinglePacket jinglePacket) {
+ Log.d(Config.LOGTAG, "<-- " + jinglePacket);
+ respondOk(jinglePacket);
+ final var sessionInfo = FileTransferDescription.getSessionInfo(jinglePacket);
+ if (sessionInfo instanceof FileTransferDescription.Checksum checksum) {
+ receiveSessionInfoChecksum(checksum);
+ } else if (sessionInfo instanceof FileTransferDescription.Received received) {
+ receiveSessionInfoReceived(received);
+ }
+ }
- final Element fileOffer = this.description.getFileOffer();
+ private void receiveSessionInfoChecksum(final FileTransferDescription.Checksum checksum) {
+ Log.d(Config.LOGTAG, "received checksum " + checksum);
+ }
- if (fileOffer != null) {
- boolean remoteIsUsingJet = false;
- Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
- if (encrypted == null) {
- final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
- if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received jingle file offer with JET");
- encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX);
- remoteIsUsingJet = true;
- }
- }
- if (encrypted != null) {
- this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
- }
- Element fileSize = fileOffer.findChild("size");
- final String path = fileOffer.findChildContent("name");
- if (path != null) {
- AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path);
- if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) {
- message.setType(Message.TYPE_IMAGE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message, message.getUuid() + "." + extension.main);
- } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
- if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) {
- message.setType(Message.TYPE_IMAGE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary);
- } else {
- message.setType(Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : ""));
- }
- message.setEncryption(Message.ENCRYPTION_PGP);
- } else {
- message.setType(Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : ""));
- }
- long size = parseLong(fileSize, 0);
- Message.FileParams fp = new Message.FileParams();
- fp.size = new Long(size);
- message.setFileParams(fp);
- conversation.add(message);
- jingleConnectionManager.updateConversationUi(true);
- this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
- if (mXmppAxolotlMessage != null) {
- XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
- if (transportMessage != null) {
- message.setEncryption(Message.ENCRYPTION_AXOLOTL);
- this.file.setKey(transportMessage.getKey());
- this.file.setIv(transportMessage.getIv());
- message.setFingerprint(transportMessage.getFingerprint());
- } else {
- Log.d(Config.LOGTAG, "could not process KeyTransportMessage");
- }
- }
- //legacy OMEMO encrypted file transfers reported the file size after encryption
- //JET reports the plain text size. however lower levels of our receiving code still
- //expect the cipher text size. so we just + 16 bytes (auth tag size) here
- this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
-
- respondToIq(packet, true);
-
- if (id.account.getRoster().getContact(id.with).showInContactList()
- && jingleConnectionManager.hasStoragePermission()
- && size < this.jingleConnectionManager.getAutoAcceptFileSize()
- && xmppConnectionService.isDataSaverDisabled()) {
- Log.d(Config.LOGTAG, "auto accepting file from " + id.with);
- this.acceptedAutomatically = true;
- this.sendAccept();
- } else {
- message.markUnread();
- Log.d(Config.LOGTAG,
- "not auto accepting new file offer with size: "
- + size
- + " allowed size:"
- + this.jingleConnectionManager
- .getAutoAcceptFileSize());
- this.xmppConnectionService.getNotificationService().push(message);
- }
- Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
- return;
- }
- respondToIq(packet, false);
+ private void receiveSessionInfoReceived(final FileTransferDescription.Received received) {
+ Log.d(Config.LOGTAG, "peer confirmed received " + received);
+ }
+
+ private void receiveSessionTerminate(final JinglePacket jinglePacket) {
+ final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason();
+ final State previous = this.state;
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": received session terminate reason="
+ + wrapper.reason
+ + "("
+ + Strings.nullToEmpty(wrapper.text)
+ + ") while in state "
+ + previous);
+ if (TERMINATED.contains(previous)) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": ignoring session terminate because already in "
+ + previous);
+ return;
}
+ if (isInitiator()) {
+ this.message.setErrorMessage(
+ Strings.isNullOrEmpty(wrapper.text) ? wrapper.reason.toString() : wrapper.text);
+ }
+ terminateTransport();
+ final State target = reasonToState(wrapper.reason);
+ transitionOrThrow(target);
+ finish();
}
- private void setupDescription(final FileTransferDescription.Version version) {
- this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
- final FileTransferDescription description;
- if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
- this.file.setKey(mXmppAxolotlMessage.getInnerKey());
- this.file.setIv(mXmppAxolotlMessage.getIV());
- //legacy OMEMO encrypted file transfer reported file size of the encrypted file
- //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag)
- this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16));
- if (remoteSupportsOmemoJet) {
- description = FileTransferDescription.of(this.file, version, null);
- } else {
- description = FileTransferDescription.of(this.file, version, this.mXmppAxolotlMessage);
- }
+ private void receiveTransportAccept(final JinglePacket jinglePacket) {
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT);
+ return;
+ }
+ Log.d(Config.LOGTAG, "receive transport accept " + jinglePacket);
+ final GenericTransportInfo transportInfo;
+ try {
+ transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ if (isInState(State.SESSION_ACCEPTED)) {
+ final var group = jinglePacket.getGroup();
+ receiveTransportAccept(jinglePacket, new Transport.TransportInfo(transportInfo, group));
} else {
- this.file.setExpectedSize(file.getSize());
- description = FileTransferDescription.of(this.file, version, null);
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT);
}
- this.description = description;
}
- private void sendInitRequest() {
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET");
- final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
- security.setAttribute("name", this.contentName);
- security.setAttribute("cipher", JET_OMEMO_CIPHER);
- security.setAttribute("type", AxolotlService.PEP_PREFIX);
- security.addChild(mXmppAxolotlMessage.toElement());
- content.addChild(security);
+ private void receiveTransportAccept(
+ final JinglePacket jinglePacket, final Transport.TransportInfo transportInfo) {
+ final FileTransferContentMap remoteContentMap =
+ getRemoteContentMap().withTransport(transportInfo);
+ setRemoteContentMap(remoteContentMap);
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (configureTransportWithPeerInfo(transport, remoteContentMap)) {
+ transport.connect();
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ "Transport in transport-accept did not match our transport-replace");
+ terminateTransport();
+ sendSessionTerminate(
+ Reason.FAILED_APPLICATION,
+ "Transport in transport-accept did not match our transport-replace");
}
- content.setDescription(this.description);
+ }
+
+ private void receiveTransportInfo(final JinglePacket jinglePacket) {
+ final FileTransferContentMap contentMap;
+ final GenericTransportInfo transportInfo;
try {
- this.mFileInputStream = new FileInputStream(file);
- } catch (FileNotFoundException e) {
- fail(e.getMessage());
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ transportInfo = contentMap.requireOnlyTransportInfo();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
return;
}
- if (this.initialTransport == IbbTransportInfo.class) {
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer");
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (transport instanceof SocksByteStreamsTransport socksBytestreamsTransport
+ && transportInfo
+ instanceof SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ receiveTransportInfo(socksBytestreamsTransport, socksBytestreamsTransportInfo);
+ } else if (transport instanceof WebRTCDataChannelTransport webRTCDataChannelTransport
+ && transportInfo
+ instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ receiveTransportInfo(
+ Iterables.getOnlyElement(contentMap.contents.keySet()),
+ webRTCDataChannelTransport,
+ webRTCDataChannelTransportInfo);
+ } else if (transportInfo
+ instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ receiveTransportInfo(
+ Iterables.getOnlyElement(contentMap.contents.keySet()),
+ webRTCDataChannelTransportInfo);
} else {
- final Collection candidates = getOurCandidates();
- content.setTransport(new S5BTransportInfo(this.transportId, candidates));
- Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size()));
- }
- packet.addJingleContent(content);
- this.sendJinglePacket(packet, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer");
- if (mJingleStatus == JINGLE_STATUS_OFFERED) {
- mJingleStatus = JINGLE_STATUS_INITIATED;
- xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
- } else {
- Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus);
- }
- } else {
- fail(IqParser.extractErrorMessage(response));
- }
- });
-
+ Log.d(Config.LOGTAG, "could not deliver transport-info to transport");
+ }
}
- private void sendHash() {
- final Element checksum = new Element("checksum", description.getVersion().getNamespace());
- checksum.setAttribute("creator", "initiator");
- checksum.setAttribute("name", "a-file-offer");
- Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2");
- hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP));
+ private void receiveTransportInfo(
+ final String contentName,
+ final WebRTCDataChannelTransport webRTCDataChannelTransport,
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ final var credentials = webRTCDataChannelTransportInfo.getCredentials();
+ final var iceCandidates =
+ WebRTCDataChannelTransport.iceCandidatesOf(
+ contentName, credentials, webRTCDataChannelTransportInfo.getCandidates());
+ final var localContentMap = getLocalContentMap();
+ if (localContentMap == null) {
+ Log.d(Config.LOGTAG, "transport not ready. add pending ice candidate");
+ this.pendingIncomingIceCandidates.addAll(iceCandidates);
+ } else {
+ webRTCDataChannelTransport.addIceCandidates(iceCandidates);
+ }
+ }
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO);
- packet.addJingleChild(checksum);
- xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.ERROR) {
- Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)");
- }
- });
+ private void receiveTransportInfo(
+ final String contentName,
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ final var credentials = webRTCDataChannelTransportInfo.getCredentials();
+ final var iceCandidates =
+ WebRTCDataChannelTransport.iceCandidatesOf(
+ contentName, credentials, webRTCDataChannelTransportInfo.getCandidates());
+ this.pendingIncomingIceCandidates.addAll(iceCandidates);
}
- private Collection getOurCandidates() {
- return Collections2.filter(this.candidates, c -> c != null && c.isOurs());
+ private void receiveTransportInfo(
+ final SocksByteStreamsTransport socksBytestreamsTransport,
+ final SocksByteStreamsTransportInfo socksBytestreamsTransportInfo) {
+ final var transportInfo = socksBytestreamsTransportInfo.getTransportInfo();
+ if (transportInfo instanceof SocksByteStreamsTransportInfo.CandidateError) {
+ socksBytestreamsTransport.setCandidateError();
+ } else if (transportInfo
+ instanceof SocksByteStreamsTransportInfo.CandidateUsed candidateUsed) {
+ if (!socksBytestreamsTransport.setCandidateUsed(candidateUsed.cid)) {
+ sendSessionTerminate(
+ Reason.FAILED_TRANSPORT,
+ String.format(
+ "Peer is not connected to our candidate %s", candidateUsed.cid));
+ }
+ } else if (transportInfo instanceof SocksByteStreamsTransportInfo.Activated activated) {
+ socksBytestreamsTransport.setProxyActivated(activated.cid);
+ } else if (transportInfo instanceof SocksByteStreamsTransportInfo.ProxyError) {
+ socksBytestreamsTransport.setProxyError();
+ }
}
- private void sendAccept() {
- mJingleStatus = JINGLE_STATUS_ACCEPTED;
- this.mStatus = Transferable.STATUS_DOWNLOADING;
- this.jingleConnectionManager.updateConversationUi(true);
- if (initialTransport == S5BTransportInfo.class) {
- sendAcceptSocks();
+ private void receiveTransportReplace(final JinglePacket jinglePacket) {
+ if (isInitiator()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE);
+ return;
+ }
+ Log.d(Config.LOGTAG, "receive transport replace " + jinglePacket);
+ final GenericTransportInfo transportInfo;
+ try {
+ transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ if (isInState(State.SESSION_ACCEPTED)) {
+ receiveTransportReplace(jinglePacket, transportInfo);
} else {
- sendAcceptIbb();
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE);
}
}
- private void sendAcceptSocks() {
- gatherAndConnectDirectCandidates();
- this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setDescription(this.description);
- if (success && candidate != null && !equalCandidateExists(candidate)) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
+ private void receiveTransportReplace(
+ final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) {
+ respondOk(jinglePacket);
+ final Transport transport;
+ try {
+ transport = setupTransport(transportInfo);
+ } catch (final RuntimeException e) {
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ this.transport = transport;
+ this.transport.setTransportCallback(this);
+ final var transportInfoFuture = transport.asTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
@Override
- public void failed() {
- Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
+ public void onSuccess(final Transport.TransportInfo transportWrapper) {
+ final FileTransferContentMap contentMap =
+ getLocalContentMap().withTransport(transportWrapper);
+ sendTransportAccept(contentMap);
}
@Override
- public void established() {
- Log.d(Config.LOGTAG, "connected to proxy65 candidate");
- mergeCandidate(candidate);
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
+ public void onFailure(@NonNull Throwable throwable) {
+ // transition into application failed (analogues to failureToAccept
}
- });
- } else {
- Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves");
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
- }
- });
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void sendTransportAccept(final FileTransferContentMap contentMap) {
+ setLocalContentMap(contentMap);
+ final var jinglePacket =
+ contentMap
+ .transportInfo()
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_ACCEPT, id.sessionId);
+ Log.d(Config.LOGTAG, "sending transport accept " + jinglePacket);
+ send(jinglePacket);
+ transport.connect();
+ }
+
+ protected void sendSessionTerminate(final Reason reason, final String text) {
+ if (isInitiator()) {
+ this.message.setErrorMessage(Strings.isNullOrEmpty(text) ? reason.toString() : text);
+ }
+ sendSessionTerminate(reason, text, null);
+ }
+
+ private FileTransferContentMap getLocalContentMap() {
+ return isInitiator()
+ ? this.initiatorFileTransferContentMap
+ : this.responderFileTransferContentMap;
}
- private void sendAcceptIbb() {
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setDescription(this.description);
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- packet.addJingleContent(content);
- this.transport.receive(file, onFileTransmissionStatusChanged);
- this.sendJinglePacket(packet);
+ private FileTransferContentMap getRemoteContentMap() {
+ return isInitiator()
+ ? this.responderFileTransferContentMap
+ : this.initiatorFileTransferContentMap;
}
- private JinglePacket bootstrapPacket(JinglePacket.Action action) {
- final JinglePacket packet = new JinglePacket(action, this.id.sessionId);
- packet.setTo(id.with);
- return packet;
+ private void setLocalContentMap(final FileTransferContentMap contentMap) {
+ if (isInitiator()) {
+ this.initiatorFileTransferContentMap = contentMap;
+ } else {
+ this.responderFileTransferContentMap = contentMap;
+ }
}
- private void sendJinglePacket(JinglePacket packet) {
- xmppConnectionService.sendIqPacket(id.account, packet, responseListener);
+ private void setRemoteContentMap(final FileTransferContentMap contentMap) {
+ if (isInitiator()) {
+ this.responderFileTransferContentMap = contentMap;
+ } else {
+ this.initiatorFileTransferContentMap = contentMap;
+ }
}
- private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
- xmppConnectionService.sendIqPacket(id.account, packet, callback);
+ public Transport getTransport() {
+ return this.transport;
}
- private void receiveAccept(JinglePacket packet) {
- if (responding()) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept (we were responding)");
- respondToIqWithOutOfOrder(packet);
+ @Override
+ protected void terminateTransport() {
+ final var transport = this.transport;
+ if (transport == null) {
return;
}
- if (this.mJingleStatus != JINGLE_STATUS_INITIATED) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept");
- respondToIqWithOutOfOrder(packet);
+ transport.terminate();
+ this.transport = null;
+ }
+
+ @Override
+ void notifyRebound() {}
+
+ @Override
+ public void onTransportEstablished() {
+ Log.d(Config.LOGTAG, "on transport established");
+ final AbstractFileTransceiver fileTransceiver;
+ try {
+ fileTransceiver = setupTransceiver(isResponder());
+ } catch (final Exception e) {
+ Log.d(Config.LOGTAG, "failed to set up file transceiver", e);
+ sendSessionTerminate(Reason.ofThrowable(e), e.getMessage());
return;
}
- this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
- xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- //TODO we want to fail if transportInfo doesn’t match our intialTransport and/or our id
- if (transportInfo instanceof S5BTransportInfo) {
- final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
- respondToIq(packet, true);
- //TODO calling merge is probably a bug because that might eliminate candidates of the other party and lead to us not sending accept/deny
- //TODO: we probably just want to call add
- mergeCandidates(s5BTransportInfo.getCandidates());
- this.connectNextCandidate();
- } else if (transportInfo instanceof IbbTransportInfo) {
- final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
- final int remoteBlockSize = ibbTransportInfo.getBlockSize();
- if (remoteBlockSize > 0) {
- this.ibbBlockSize = Math.min(ibbBlockSize, remoteBlockSize);
- }
- respondToIq(packet, true);
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
- this.transport.connect(onIbbTransportConnected);
+ this.fileTransceiver = fileTransceiver;
+ final var fileTransceiverThread = new Thread(fileTransceiver);
+ fileTransceiverThread.start();
+ Futures.addCallback(
+ fileTransceiver.complete,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final List hashes) {
+ onFileTransmissionComplete(hashes);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ onFileTransmissionFailed(throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void onFileTransmissionComplete(final List hashes) {
+ // TODO if we ever support receiving files this should become isSending(); isReceiving()
+ if (isInitiator()) {
+ sendSessionInfoChecksum(hashes);
} else {
- respondToIq(packet, false);
+ Log.d(Config.LOGTAG, "file transfer complete " + hashes);
+ sendFileSessionInfoReceived();
+ terminateTransport();
+ messageReceivedSuccess();
+ sendSessionTerminate(Reason.SUCCESS, null);
}
}
- private void receiveTransportInfo(JinglePacket packet) {
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- if (transportInfo instanceof S5BTransportInfo) {
- final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo;
- if (s5BTransportInfo.hasChild("activated")) {
- respondToIq(packet, true);
- if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) {
- onProxyActivated.success();
- } else {
- String cid = s5BTransportInfo.findChild("activated").getAttribute("cid");
- Log.d(Config.LOGTAG, "received proxy activated (" + cid
- + ")prior to choosing our own transport");
- JingleSocks5Transport connection = this.connections.get(cid);
- if (connection != null) {
- connection.setActivated(true);
- } else {
- Log.d(Config.LOGTAG, "activated connection not found");
- sendSessionTerminate(Reason.FAILED_TRANSPORT);
- this.fail();
- }
- }
- } else if (s5BTransportInfo.hasChild("proxy-error")) {
- respondToIq(packet, true);
- onProxyActivated.failed();
- } else if (s5BTransportInfo.hasChild("candidate-error")) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error");
- respondToIq(packet, true);
- this.receivedCandidate = true;
- if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
- this.connect();
- }
- } else if (s5BTransportInfo.hasChild("candidate-used")) {
- String cid = s5BTransportInfo.findChild("candidate-used").getAttribute("cid");
- if (cid != null) {
- Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
- JingleCandidate candidate = getCandidate(cid);
- if (candidate == null) {
- Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
- respondToIq(packet, false);
- return;
- }
- respondToIq(packet, true);
- candidate.flagAsUsedByCounterpart();
- this.receivedCandidate = true;
- if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
- this.connect();
- } else {
- Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
- }
- } else {
- respondToIq(packet, false);
- }
+ private void messageReceivedSuccess() {
+ this.message.setTransferable(null);
+ xmppConnectionService.getFileBackend().updateFileParams(message);
+ xmppConnectionService.databaseBackend.createMessage(message);
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ if (acceptedAutomatically) {
+ message.markUnread();
+ if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ id.account.getPgpDecryptionService().decrypt(message, true);
} else {
- respondToIq(packet, false);
+ xmppConnectionService
+ .getFileBackend()
+ .updateMediaScanner(
+ file,
+ () ->
+ JingleFileTransferConnection.this
+ .xmppConnectionService
+ .getNotificationService()
+ .push(message));
}
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ id.account.getPgpDecryptionService().decrypt(message, false);
} else {
- respondToIq(packet, true);
+ xmppConnectionService.getFileBackend().updateMediaScanner(file);
}
}
- private void connect() {
- final JingleSocks5Transport connection = chooseConnection();
- this.transport = connection;
- if (connection == null) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate");
- this.disconnectSocks5Connections();
- if (isInitiator()) {
- this.sendFallbackToIbb();
- }
+ private void onFileTransmissionFailed(final Throwable throwable) {
+ if (isTerminated()) {
+ Log.d(
+ Config.LOGTAG,
+ "file transfer failed but session is already terminated",
+ throwable);
} else {
- //TODO at this point we can already close other connections to free some resources
- final JingleCandidate candidate = connection.getCandidate();
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString());
- this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
- if (connection.needsActivation()) {
- if (connection.getCandidate().isOurs()) {
- final String sid;
- if (description.getVersion() == FileTransferDescription.Version.FT_3) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy");
- sid = id.sessionId;
- } else {
- sid = getTransportId();
- }
- Log.d(Config.LOGTAG, "candidate "
- + connection.getCandidate().getCid()
- + " was our proxy. going to activate");
- IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
- activation.setTo(connection.getCandidate().getJid());
- activation.query("http://jabber.org/protocol/bytestreams")
- .setAttribute("sid", sid);
- activation.query().addChild("activate")
- .setContent(this.id.with.toEscapedString());
- xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> {
- if (response.getType() != IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString());
- sendProxyError();
- onProxyActivated.failed();
- } else {
- sendProxyActivated(connection.getCandidate().getCid());
- onProxyActivated.success();
- }
- });
- } else {
- Log.d(Config.LOGTAG,
- "candidate "
- + connection.getCandidate().getCid()
- + " was a proxy. waiting for other party to activate");
- }
- } else {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, "we were initiating. sending file");
- connection.send(file, onFileTransmissionStatusChanged);
- } else {
- Log.d(Config.LOGTAG, "we were responding. receiving file");
- connection.receive(file, onFileTransmissionStatusChanged);
- }
- }
+ terminateTransport();
+ Log.d(Config.LOGTAG, "on file transmission failed", throwable);
+ sendSessionTerminate(Reason.CONNECTIVITY_ERROR, null);
}
}
- private JingleSocks5Transport chooseConnection() {
- final List establishedConnections = FluentIterable.from(connections.entrySet())
- .transform(Entry::getValue)
- .filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs())))
- .toSortedList((a, b) -> {
- final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority());
- if (compare == 0) {
- if (isInitiator()) {
- //pick the one we sent a candidate-used for (meaning not ours)
- return a.getCandidate().isOurs() ? 1 : -1;
- } else {
- //pick the one they sent a candidate-used for (meaning ours)
- return a.getCandidate().isOurs() ? -1 : 1;
- }
- }
- return compare;
- });
- return Iterables.getFirst(establishedConnections, null);
+ private AbstractFileTransceiver setupTransceiver(final boolean receiving) throws IOException {
+ final var fileDescription = getLocalContentMap().requireOnlyFile();
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ final Runnable updateRunnable = () -> jingleConnectionManager.updateConversationUi(false);
+ if (receiving) {
+ return new FileReceiver(
+ file,
+ this.transportSecurity,
+ transport.getInputStream(),
+ transport.getTerminationLatch(),
+ fileDescription.size,
+ updateRunnable);
+ } else {
+ return new FileTransmitter(
+ file,
+ this.transportSecurity,
+ transport.getOutputStream(),
+ transport.getTerminationLatch(),
+ fileDescription.size,
+ updateRunnable);
+ }
}
- private void sendSuccess() {
- sendSessionTerminate(Reason.SUCCESS);
- this.disconnectSocks5Connections();
- this.mJingleStatus = JINGLE_STATUS_FINISHED;
- this.message.setStatus(Message.STATUS_RECEIVED);
- this.message.setTransferable(null);
- this.xmppConnectionService.updateMessage(message, false);
- this.jingleConnectionManager.finishConnection(this);
+ private void sendFileSessionInfoReceived() {
+ final var contentMap = getLocalContentMap();
+ final String name = Iterables.getOnlyElement(contentMap.contents.keySet());
+ sendSessionInfo(new FileTransferDescription.Received(name));
}
- private void sendFallbackToIbb() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- this.transportId = JingleConnectionManager.nextRandomId();
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
+ private void sendSessionInfoChecksum(List hashes) {
+ final var contentMap = getLocalContentMap();
+ final String name = Iterables.getOnlyElement(contentMap.contents.keySet());
+ sendSessionInfo(new FileTransferDescription.Checksum(name, hashes));
}
+ private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) {
+ final var jinglePacket =
+ new JinglePacket(JinglePacket.Action.SESSION_INFO, this.id.sessionId);
+ jinglePacket.addJingleChild(sessionInfo.asElement());
+ jinglePacket.setTo(this.id.with);
+ Log.d(Config.LOGTAG, "--> " + jinglePacket);
+ send(jinglePacket);
+ }
- private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)");
- respondToIqWithOutOfOrder(packet);
+ @Override
+ public void onTransportSetupFailed() {
+ final var transport = this.transport;
+ if (transport == null) {
+ // this really is not supposed to happen
+ sendSessionTerminate(Reason.FAILED_APPLICATION, null);
return;
}
- final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
- if (!validState) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace");
- respondToIqWithOutOfOrder(packet);
+ Log.d(Config.LOGTAG, "onTransportSetupFailed");
+ final var isTransportInBand = transport instanceof InbandBytestreamsTransport;
+ if (isTransportInBand) {
+ terminateTransport();
+ sendSessionTerminate(Reason.CONNECTIVITY_ERROR, "Failed to setup IBB transport");
return;
}
- this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one;
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb");
- final int remoteBlockSize = transportInfo.getBlockSize();
- if (remoteBlockSize > 0) {
- this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize);
+ // terminate the current transport
+ transport.terminate();
+ if (isInitiator()) {
+ this.transport = setupLastResortTransport();
+ this.transport.setTransportCallback(this);
+ final var transportInfoFuture = this.transport.asTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Transport.TransportInfo transportWrapper) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ sendTransportReplace(contentMap.withTransport(transportWrapper));
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ // TODO send application failure;
+ }
+ },
+ MoreExecutors.directExecutor());
+
} else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace");
+ Log.d(Config.LOGTAG, "transport setup failed. waiting for initiator to replace");
}
- this.transportId = transportInfo.getTransportId(); //TODO: handle the case where this is null by the remote party
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
-
- final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT);
+ }
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- answer.addJingleContent(content);
+ private void sendTransportReplace(final FileTransferContentMap contentMap) {
+ setLocalContentMap(contentMap);
+ final var jinglePacket =
+ contentMap
+ .transportInfo()
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_REPLACE, id.sessionId);
+ Log.d(Config.LOGTAG, "sending transport replace " + jinglePacket);
+ send(jinglePacket);
+ }
- respondToIq(packet, true);
+ @Override
+ public void onAdditionalCandidate(
+ final String contentName, final Transport.Candidate candidate) {
+ if (candidate instanceof IceUdpTransportInfo.Candidate iceCandidate) {
+ sendTransportInfo(contentName, iceCandidate);
+ }
+ }
- if (isInitiator()) {
- this.sendJinglePacket(answer, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.RESULT) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb");
- transport.connect(onIbbTransportConnected);
- }
- });
- } else {
- this.transport.receive(file, onFileTransmissionStatusChanged);
- this.sendJinglePacket(answer);
+ public void sendTransportInfo(
+ final String contentName, final IceUdpTransportInfo.Candidate candidate) {
+ final FileTransferContentMap transportInfo;
+ try {
+ final FileTransferContentMap rtpContentMap = getLocalContentMap();
+ transportInfo = rtpContentMap.transportInfo(contentName, candidate);
+ } catch (final Exception e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": unable to prepare transport-info from candidate for content="
+ + contentName);
+ return;
}
+ final JinglePacket jinglePacket =
+ transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ Log.d(Config.LOGTAG, "--> " + jinglePacket);
+ send(jinglePacket);
}
- private void receiveTransportAccept(JinglePacket packet) {
- if (responding()) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)");
- respondToIqWithOutOfOrder(packet);
+ @Override
+ public void onCandidateUsed(
+ final String streamId, final SocksByteStreamsTransport.Candidate candidate) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ if (contentMap == null) {
+ Log.e(Config.LOGTAG, "local content map is null on candidate used");
return;
}
- final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING);
- if (!validState) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept");
- respondToIqWithOutOfOrder(packet);
+ final var jinglePacket =
+ contentMap
+ .candidateUsed(streamId, candidate.cid)
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ Log.d(Config.LOGTAG, "sending candidate used " + jinglePacket);
+ send(jinglePacket);
+ }
+
+ @Override
+ public void onCandidateError(final String streamId) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ if (contentMap == null) {
+ Log.e(Config.LOGTAG, "local content map is null on candidate used");
return;
}
- this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one;
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content == null ? null : content.getTransport();
- if (transportInfo instanceof IbbTransportInfo) {
- final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo;
- final int remoteBlockSize = ibbTransportInfo.getBlockSize();
- if (remoteBlockSize > 0) {
- this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize);
- }
- final String sid = ibbTransportInfo.getTransportId();
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
+ final var jinglePacket =
+ contentMap
+ .candidateError(streamId)
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ Log.d(Config.LOGTAG, "sending candidate error " + jinglePacket);
+ send(jinglePacket);
+ }
- if (sid == null || !sid.equals(this.transportId)) {
- Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", id.account.getJid().asBareJid(), sid, transportId));
- }
- respondToIq(packet, true);
- //might be receive instead if we are not initiating
- if (isInitiator()) {
- this.transport.connect(onIbbTransportConnected);
- }
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept");
- respondToIq(packet, false);
+ @Override
+ public void onProxyActivated(String streamId, SocksByteStreamsTransport.Candidate candidate) {
+ final FileTransferContentMap contentMap = getLocalContentMap();
+ if (contentMap == null) {
+ Log.e(Config.LOGTAG, "local content map is null on candidate used");
+ return;
}
+ final var jinglePacket =
+ contentMap
+ .proxyActivated(streamId, candidate.cid)
+ .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId);
+ send(jinglePacket);
}
- private void receiveSuccess() {
- if (isInitiator()) {
- this.mJingleStatus = JINGLE_STATUS_FINISHED;
- this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
+ @Override
+ protected boolean transition(final State target, final Runnable runnable) {
+ final boolean transitioned = super.transition(target, runnable);
+ if (transitioned && isInitiator()) {
+ Log.d(Config.LOGTAG, "running mark message hooks");
+ if (target == State.SESSION_ACCEPTED) {
+ xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
+ } else if (target == State.TERMINATED_SUCCESS) {
+ xmppConnectionService.markMessage(message, Message.STATUS_SEND_RECEIVED);
+ } else if (TERMINATED.contains(target)) {
+ xmppConnectionService.markMessage(
+ message, Message.STATUS_SEND_FAILED, message.getErrorMessage());
+ } else {
+ xmppConnectionService.updateConversationUi();
}
- this.message.setTransferable(null);
- this.jingleConnectionManager.finishConnection(this);
} else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding");
+ if (Arrays.asList(State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)
+ .contains(target)) {
+ this.message.setTransferable(
+ new TransferablePlaceholder(Transferable.STATUS_CANCELLED));
+ } else if (target != State.TERMINATED_SUCCESS && TERMINATED.contains(target)) {
+ this.message.setTransferable(
+ new TransferablePlaceholder(Transferable.STATUS_FAILED));
+ }
+ xmppConnectionService.updateConversationUi();
}
+ return transitioned;
}
@Override
- public void cancel() {
- this.cancelled = true;
- abort(Reason.CANCEL);
- }
-
- private void abort(final Reason reason) {
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
+ protected void finish() {
+ if (transport != null) {
+ throw new AssertionError(
+ "finish MUST not be called without terminating the transport first");
}
- sendSessionTerminate(reason);
- this.jingleConnectionManager.finishConnection(this);
- if (responding()) {
- this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
- if (this.file != null) {
- file.delete();
- }
- this.jingleConnectionManager.updateConversationUi(true);
- } else {
- this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
+ // we don't want to remove TransferablePlaceholder
+ if (message.getTransferable() instanceof JingleFileTransferConnection) {
+ Log.d(Config.LOGTAG, "nulling transferable on message");
this.message.setTransferable(null);
}
+ super.finish();
}
- private void fail() {
- fail(null);
- }
-
- private void fail(String errorMessage) {
- this.mJingleStatus = JINGLE_STATUS_FAILED;
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
- }
- FileBackend.close(mFileInputStream);
- FileBackend.close(mFileOutputStream);
- if (this.message != null) {
- if (responding()) {
- this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
- if (this.file != null) {
- file.delete();
- }
- this.jingleConnectionManager.updateConversationUi(true);
- } else {
- this.xmppConnectionService.markMessage(this.message,
- Message.STATUS_SEND_FAILED,
- cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
- this.message.setTransferable(null);
- }
+ private int getTransferableStatus() {
+ // status in file transfer is a bit weird. for sending it is mostly handled via
+ // Message.STATUS_* (offered, unsend (sic) send_received) the transferable status is just
+ // uploading
+ // for receiving the message status remains at 'received' but Transferable goes through
+ // various status
+ if (isInitiator()) {
+ return Transferable.STATUS_UPLOADING;
}
- this.jingleConnectionManager.finishConnection(this);
+ final var state = getState();
+ return switch (state) {
+ case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED -> Transferable
+ .STATUS_OFFER;
+ case TERMINATED_APPLICATION_FAILURE,
+ TERMINATED_CONNECTIVITY_ERROR,
+ TERMINATED_DECLINED_OR_BUSY,
+ TERMINATED_SECURITY_ERROR -> Transferable.STATUS_FAILED;
+ case TERMINATED_CANCEL_OR_TIMEOUT -> Transferable.STATUS_CANCELLED;
+ case SESSION_ACCEPTED -> Transferable.STATUS_DOWNLOADING;
+ default -> Transferable.STATUS_UNKNOWN;
+ };
}
- private void sendSessionTerminate(Reason reason) {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE);
- packet.setReason(reason, null);
- this.sendJinglePacket(packet);
- }
+ // these methods are for interacting with 'Transferable' - we might want to remove the concept
+ // at some point
- private void connectNextCandidate() {
- for (JingleCandidate candidate : this.candidates) {
- if ((!connections.containsKey(candidate.getCid()) && (!candidate
- .isOurs()))) {
- this.connectWithCandidate(candidate);
- return;
- }
+ @Override
+ public boolean start() {
+ Log.d(Config.LOGTAG, "user pressed start()");
+ // TODO there is a 'connected' check apparently?
+ if (isInState(State.SESSION_INITIALIZED)) {
+ sendSessionAccept();
}
- this.sendCandidateError();
+ return true;
}
- private void connectWithCandidate(final JingleCandidate candidate) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
- this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG,
- "connection failed with " + candidate.getHost() + ":"
- + candidate.getPort());
- connectNextCandidate();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG,
- "established connection with " + candidate.getHost()
- + ":" + candidate.getPort());
- sendCandidateUsed(candidate.getCid());
- }
- });
+ @Override
+ public int getStatus() {
+ return getTransferableStatus();
}
- private void disconnectSocks5Connections() {
- Iterator> it = this.connections
- .entrySet().iterator();
- while (it.hasNext()) {
- Entry pairs = it.next();
- pairs.getValue().disconnect();
- it.remove();
+ @Override
+ public Long getFileSize() {
+ final var transceiver = this.fileTransceiver;
+ if (transceiver != null) {
+ return transceiver.total;
+ }
+ final var contentMap = this.initiatorFileTransferContentMap;
+ if (contentMap != null) {
+ return contentMap.requireOnlyFile().size;
}
+ return null;
}
- private void sendProxyActivated(String cid) {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid)));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
+ @Override
+ public int getProgress() {
+ final var transceiver = this.fileTransceiver;
+ return transceiver != null ? transceiver.getProgress() : 0;
}
- private void sendProxyError() {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error")));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
+ @Override
+ public void cancel() {
+ if (stopFileTransfer()) {
+ Log.d(Config.LOGTAG, "user has stopped file transfer");
+ } else {
+ Log.d(Config.LOGTAG, "user pressed cancel but file transfer was already terminated?");
+ }
}
- private void sendCandidateUsed(final String cid) {
- JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid)));
- packet.addJingleContent(content);
- this.sentCandidate = true;
- if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
- connect();
+ private boolean stopFileTransfer() {
+ if (isInitiator()) {
+ return stopFileTransfer(Reason.CANCEL);
+ } else {
+ return stopFileTransfer(Reason.DECLINE);
}
- this.sendJinglePacket(packet);
}
- private void sendCandidateError() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error");
- JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error")));
- packet.addJingleContent(content);
- this.sentCandidate = true;
- this.sendJinglePacket(packet);
- if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
- connect();
+ private boolean stopFileTransfer(final Reason reason) {
+ final State target = reasonToState(reason);
+ if (transition(target)) {
+ // we change state before terminating transport so we don't consume the following
+ // IOException and turn it into a connectivity error
+ terminateTransport();
+ final JinglePacket jinglePacket =
+ new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
+ jinglePacket.setReason(reason, "User requested to stop file transfer");
+ send(jinglePacket);
+ finish();
+ return true;
+ } else {
+ return false;
}
}
- private int getJingleStatus() {
- return this.mJingleStatus;
- }
+ private abstract static class AbstractFileTransceiver implements Runnable {
+
+ protected final SettableFuture> complete =
+ SettableFuture.create();
+
+ protected final File file;
+ protected final TransportSecurity transportSecurity;
+
+ protected final CountDownLatch transportTerminationLatch;
+ protected final long total;
+ protected long transmitted = 0;
+ private int progress = Integer.MIN_VALUE;
+ private final Runnable updateRunnable;
+
+ private AbstractFileTransceiver(
+ final File file,
+ final TransportSecurity transportSecurity,
+ final CountDownLatch transportTerminationLatch,
+ final long total,
+ final Runnable updateRunnable) {
+ this.file = file;
+ this.transportSecurity = transportSecurity;
+ this.transportTerminationLatch = transportTerminationLatch;
+ this.total = transportSecurity == null ? total : (total + 16);
+ this.updateRunnable = updateRunnable;
+ }
- private boolean equalCandidateExists(JingleCandidate candidate) {
- for (JingleCandidate c : this.candidates) {
- if (c.equalValues(candidate)) {
- return true;
+ static void closeTransport(final Closeable stream) {
+ try {
+ stream.close();
+ } catch (final IOException e) {
+ Log.d(Config.LOGTAG, "transport has already been closed. good");
}
}
- return false;
- }
- private void mergeCandidate(JingleCandidate candidate) {
- for (JingleCandidate c : this.candidates) {
- if (c.equals(candidate)) {
- return;
- }
+ public int getProgress() {
+ return Ints.saturatedCast(Math.round((1.0 * transmitted / total) * 100));
}
- this.candidates.add(candidate);
- }
- private void mergeCandidates(List candidates) {
- Collections.sort(candidates, (a, b) -> Integer.compare(b.getPriority(), a.getPriority()));
- for (JingleCandidate c : candidates) {
- mergeCandidate(c);
+ public void updateProgress() {
+ final int current = getProgress();
+ final boolean update;
+ synchronized (this) {
+ if (this.progress != current) {
+ this.progress = current;
+ update = true;
+ } else {
+ update = false;
+ }
+ if (update) {
+ this.updateRunnable.run();
+ }
+ }
}
- }
- private JingleCandidate getCandidate(String cid) {
- for (JingleCandidate c : this.candidates) {
- if (c.getCid().equals(cid)) {
- return c;
+ protected void awaitTransportTermination() {
+ try {
+ this.transportTerminationLatch.await();
+ } catch (final InterruptedException ignored) {
+ return;
}
+ Log.d(Config.LOGTAG, getClass().getSimpleName() + " says Goodbye!");
}
- return null;
}
- void updateProgress(int i) {
- this.mProgress = i;
- jingleConnectionManager.updateConversationUi(false);
- }
+ private static class FileTransmitter extends AbstractFileTransceiver {
- String getTransportId() {
- return this.transportId;
- }
+ private final OutputStream outputStream;
- FileTransferDescription.Version getFtVersion() {
- return this.description.getVersion();
- }
+ private FileTransmitter(
+ final File file,
+ final TransportSecurity transportSecurity,
+ final OutputStream outputStream,
+ final CountDownLatch transportTerminationLatch,
+ final long total,
+ final Runnable updateRunnable) {
+ super(file, transportSecurity, transportTerminationLatch, total, updateRunnable);
+ this.outputStream = outputStream;
+ }
- public JingleTransport getTransport() {
- return this.transport;
- }
+ private InputStream openFileInputStream() throws FileNotFoundException {
+ final var fileInputStream = new FileInputStream(this.file);
+ if (this.transportSecurity == null) {
+ return fileInputStream;
+ } else {
+ final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(
+ true,
+ new AEADParameters(
+ new KeyParameter(transportSecurity.key),
+ 128,
+ transportSecurity.iv));
+ Log.d(Config.LOGTAG, "setting up CipherInputStream");
+ return new CipherInputStream(fileInputStream, cipher);
+ }
+ }
- public boolean start() {
- if (id.account.getStatus() == Account.State.ONLINE) {
- if (mJingleStatus == JINGLE_STATUS_INITIATED) {
- new Thread(this::sendAccept).start();
+ @Override
+ public void run() {
+ Log.d(Config.LOGTAG, "file transmitter attempting to send " + total + " bytes");
+ final var sha1Hasher = Hashing.sha1().newHasher();
+ final var sha256Hasher = Hashing.sha256().newHasher();
+ try (final var fileInputStream = openFileInputStream()) {
+ final var buffer = new byte[4096];
+ while (total - transmitted > 0) {
+ final int count = fileInputStream.read(buffer);
+ if (count == -1) {
+ throw new EOFException(
+ String.format("reached EOF after %d/%d", transmitted, total));
+ }
+ outputStream.write(buffer, 0, count);
+ sha1Hasher.putBytes(buffer, 0, count);
+ sha256Hasher.putBytes(buffer, 0, count);
+ transmitted += count;
+ updateProgress();
+ }
+ outputStream.flush();
+ Log.d(
+ Config.LOGTAG,
+ "transmitted " + transmitted + " bytes from " + file.getAbsolutePath());
+ final List hashes =
+ ImmutableList.of(
+ new FileTransferDescription.Hash(
+ sha1Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_1),
+ new FileTransferDescription.Hash(
+ sha256Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_256));
+ complete.set(hashes);
+ } catch (final Exception e) {
+ complete.setException(e);
}
- return true;
- } else {
- return false;
+ // the transport implementations backed by PipedOutputStreams do not like it when
+ // the writing Thread (this thread) goes away. so we just wait until the other peer
+ // has received our file and we are shutting down the transport
+ Log.d(Config.LOGTAG, "waiting for transport to terminate before stopping thread");
+ awaitTransportTermination();
+ closeTransport(outputStream);
}
}
- @Override
- public int getStatus() {
- return this.mStatus;
- }
+ private static class FileReceiver extends AbstractFileTransceiver {
- @Override
- public Long getFileSize() {
- if (this.file != null) {
- return this.file.getExpectedSize();
- } else {
- return null;
+ private final InputStream inputStream;
+
+ private FileReceiver(
+ final File file,
+ final TransportSecurity transportSecurity,
+ final InputStream inputStream,
+ final CountDownLatch transportTerminationLatch,
+ final long total,
+ final Runnable updateRunnable) {
+ super(file, transportSecurity, transportTerminationLatch, total, updateRunnable);
+ this.inputStream = inputStream;
}
- }
- @Override
- public int getProgress() {
- return this.mProgress;
- }
+ private OutputStream openFileOutputStream() throws FileNotFoundException {
+ final var directory = this.file.getParentFile();
+ if (directory != null && directory.mkdirs()) {
+ Log.d(Config.LOGTAG, "created directory " + directory.getAbsolutePath());
+ }
+ final var fileOutputStream = new FileOutputStream(this.file);
+ if (this.transportSecurity == null) {
+ return fileOutputStream;
+ } else {
+ final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
+ cipher.init(
+ false,
+ new AEADParameters(
+ new KeyParameter(transportSecurity.key),
+ 128,
+ transportSecurity.iv));
+ Log.d(Config.LOGTAG, "setting up CipherOutputStream");
+ return new CipherOutputStream(fileOutputStream, cipher);
+ }
+ }
- AbstractConnectionManager getConnectionManager() {
- return this.jingleConnectionManager;
+ @Override
+ public void run() {
+ Log.d(Config.LOGTAG, "file receiver attempting to receive " + total + " bytes");
+ final var sha1Hasher = Hashing.sha1().newHasher();
+ final var sha256Hasher = Hashing.sha256().newHasher();
+ try (final var fileOutputStream = openFileOutputStream()) {
+ final var buffer = new byte[4096];
+ while (total - transmitted > 0) {
+ final int count = inputStream.read(buffer);
+ if (count == -1) {
+ throw new EOFException(
+ String.format("reached EOF after %d/%d", transmitted, total));
+ }
+ fileOutputStream.write(buffer, 0, count);
+ sha1Hasher.putBytes(buffer, 0, count);
+ sha256Hasher.putBytes(buffer, 0, count);
+ transmitted += count;
+ updateProgress();
+ }
+ Log.d(
+ Config.LOGTAG,
+ "written " + transmitted + " bytes to " + file.getAbsolutePath());
+ final List hashes =
+ ImmutableList.of(
+ new FileTransferDescription.Hash(
+ sha1Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_1),
+ new FileTransferDescription.Hash(
+ sha256Hasher.hash().asBytes(),
+ FileTransferDescription.Algorithm.SHA_256));
+ complete.set(hashes);
+ } catch (final Exception e) {
+ complete.setException(e);
+ }
+ Log.d(Config.LOGTAG, "waiting for transport to terminate before stopping thread");
+ awaitTransportTermination();
+ closeTransport(inputStream);
+ }
}
- interface OnProxyActivated {
- void success();
+ private static final class TransportSecurity {
+ final byte[] key;
+ final byte[] iv;
- void failed();
+ private TransportSecurity(byte[] key, byte[] iv) {
+ this.key = key;
+ this.iv = iv;
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java
deleted file mode 100644
index c68941928f9939a8b248a4674eae9b959784bb34..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java
+++ /dev/null
@@ -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);
- }
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
index cb59b385caf2cb35def9036229ac68ce73e54533..d5e70d8a793016d0c74aa79528e22fe481926d54 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
@@ -14,7 +14,6 @@ import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@@ -31,14 +30,10 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.RtpSessionStatus;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Element;
@@ -82,96 +77,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
Arrays.asList(
State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED);
private static final long BUSY_TIME_OUT = 30;
- private static final List 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> VALID_TRANSITIONS;
-
- static {
- final ImmutableMap.Builder> transitionBuilder =
- new ImmutableMap.Builder<>();
- transitionBuilder.put(
- State.NULL,
- ImmutableList.of(
- State.PROPOSED,
- State.SESSION_INITIALIZED,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- transitionBuilder.put(
- State.PROPOSED,
- ImmutableList.of(
- State.ACCEPTED,
- State.PROCEED,
- State.REJECTED,
- State.RETRACTED,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR,
- State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
- // rebinds
- ));
- transitionBuilder.put(
- State.PROCEED,
- ImmutableList.of(
- State.REJECTED_RACED,
- State.RETRACTED_RACED,
- State.SESSION_INITIALIZED_PRE_APPROVED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR,
- State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
- // bounces of the proceed message
- ));
- transitionBuilder.put(
- State.SESSION_INITIALIZED,
- ImmutableList.of(
- State.SESSION_ACCEPTED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
- // and IQ timeouts
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- transitionBuilder.put(
- State.SESSION_INITIALIZED_PRE_APPROVED,
- ImmutableList.of(
- State.SESSION_ACCEPTED,
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
- // and IQ timeouts
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- transitionBuilder.put(
- State.SESSION_ACCEPTED,
- ImmutableList.of(
- State.TERMINATED_SUCCESS,
- State.TERMINATED_DECLINED_OR_BUSY,
- State.TERMINATED_CONNECTIVITY_ERROR,
- State.TERMINATED_CANCEL_OR_TIMEOUT,
- State.TERMINATED_APPLICATION_FAILURE,
- State.TERMINATED_SECURITY_ERROR));
- VALID_TRANSITIONS = transitionBuilder.build();
- }
private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this);
- private final Queue>
+ private final Queue>>
pendingIceCandidates = new LinkedList<>();
private final OmemoVerification omemoVerification = new OmemoVerification();
private final Message message;
- private State state = State.NULL;
+
private Set proposedMedia;
private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap;
@@ -197,18 +109,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.sessionId);
}
- private static State reasonToState(Reason reason) {
- return switch (reason) {
- case SUCCESS -> State.TERMINATED_SUCCESS;
- case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
- case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
- case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
- case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
- .TERMINATED_APPLICATION_FAILURE;
- default -> State.TERMINATED_CONNECTIVITY_ERROR;
- };
- }
-
@Override
synchronized void deliverPacket(final JinglePacket jinglePacket) {
switch (jinglePacket.getAction()) {
@@ -238,7 +138,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
webRTCWrapper.close();
- if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
+ if (isResponder() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) {
xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
}
if (isInState(
@@ -331,7 +231,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveTransportInfo(
final JinglePacket jinglePacket, final RtpContentMap contentMap) {
- final Set> candidates =
+ final Set>> candidates =
contentMap.contents.entrySet();
final RtpContentMap remote = getRemoteContentMap();
final Set remoteContentIds =
@@ -531,7 +431,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
setRemoteContentMap(modifiedContentMap);
- final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator());
+ final SessionDescription answer = SessionDescription.of(modifiedContentMap, isResponder());
final org.webrtc.SessionDescription sdp =
new org.webrtc.SessionDescription(
@@ -605,7 +505,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
final SessionDescription offer;
try {
- offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator());
+ offer = SessionDescription.of(modifiedRemoteContentMap, isResponder());
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(
Config.LOGTAG,
@@ -824,7 +724,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final RtpContentMap nextRemote =
currentRemote.addContent(
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
- return SessionDescription.of(nextRemote, !isInitiator());
+ return SessionDescription.of(nextRemote, isResponder());
}
throw new IllegalStateException(
"Unexpected rollback condition. Senders were not uniformly none");
@@ -890,7 +790,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final SessionDescription offer;
try {
- offer = SessionDescription.of(modifiedContentMap, !isInitiator());
+ offer = SessionDescription.of(modifiedContentMap, isResponder());
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(
Config.LOGTAG,
@@ -1075,7 +975,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
final boolean isOffer)
throws ExecutionException, InterruptedException {
final SessionDescription sessionDescription =
- SessionDescription.of(restartContentMap, !isInitiator());
+ SessionDescription.of(restartContentMap, isResponder());
final org.webrtc.SessionDescription.Type type =
isOffer
? org.webrtc.SessionDescription.Type.OFFER
@@ -1104,14 +1004,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void processCandidates(
- final Set> contents) {
- for (final Map.Entry content : contents) {
+ final Set>> contents) {
+ for (final Map.Entry> content : contents) {
processCandidate(content);
}
}
private void processCandidate(
- final Map.Entry content) {
+ final Map.Entry> content) {
final RtpContentMap rtpContentMap = getRemoteContentMap();
final List indices = toIdentificationTags(rtpContentMap);
final String sdpMid = content.getKey(); // aka content name
@@ -1213,21 +1113,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void receiveSessionInitiate(final JinglePacket jinglePacket) {
if (isInitiator()) {
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: received session-initiate even though we were initiating",
- id.account.getJid().asBareJid()));
- if (isTerminated()) {
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: got a reason to terminate with out-of-order. but already in state %s",
- id.account.getJid().asBareJid(), getState()));
- respondWithOutOfOrder(jinglePacket);
- } else {
- terminateWithOutOfOrder(jinglePacket);
- }
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE);
return;
}
final ListenableFuture future = receiveRtpContentMap(jinglePacket, false);
@@ -1309,13 +1195,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void receiveSessionAccept(final JinglePacket jinglePacket) {
- if (!isInitiator()) {
- Log.d(
- Config.LOGTAG,
- String.format(
- "%s: received session-accept even though we were responding",
- id.account.getJid().asBareJid()));
- terminateWithOutOfOrder(jinglePacket);
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
return;
}
final ListenableFuture future =
@@ -1500,7 +1381,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
private void addIceCandidatesFromBlackLog() {
- Map.Entry foo;
+ Map.Entry> foo;
while ((foo = this.pendingIceCandidates.poll()) != null) {
processCandidate(foo);
Log.d(
@@ -1537,11 +1418,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ candidates.size()
+ " candidates in session accept");
sendSessionAccept(outgoingContentMap.withCandidates(candidates));
- webRTCWrapper.resetPendingCandidates();
} else {
sendSessionAccept(outgoingContentMap);
- webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
}
+ webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
}
@Override
@@ -2003,11 +1883,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ " candidates in session initiate");
sendSessionInitiate(
outgoingContentMap.withCandidates(candidates), targetState);
- webRTCWrapper.resetPendingCandidates();
} else {
sendSessionInitiate(outgoingContentMap, targetState);
- webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
}
+ webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
}
@Override
@@ -2072,24 +1951,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- private void sendSessionTerminate(final Reason reason) {
+ protected void sendSessionTerminate(final Reason reason) {
sendSessionTerminate(reason, null);
}
- private void sendSessionTerminate(final Reason reason, final String text) {
- final State previous = this.state;
- final State target = reasonToState(reason);
- transitionOrThrow(target);
- if (previous != State.NULL) {
- writeLogMessage(target);
- }
- final JinglePacket jinglePacket =
- new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
- jinglePacket.setReason(reason, text);
- send(jinglePacket);
- finish();
+
+ protected void sendSessionTerminate(final Reason reason, final String text) {
+ sendSessionTerminate(reason,text, this::writeLogMessage);
}
+
private void sendTransportInfo(
final String contentName, IceUdpTransportInfo.Candidate candidate) {
final RtpContentMap transportInfo;
@@ -2110,110 +1981,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
send(jinglePacket);
}
- private void send(final JinglePacket jinglePacket) {
- jinglePacket.setTo(id.with);
- xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
- }
-
- private synchronized void handleIqResponse(final Account account, final IqPacket response) {
- if (response.getType() == IqPacket.TYPE.ERROR) {
- handleIqErrorResponse(response);
- return;
- }
- if (response.getType() == IqPacket.TYPE.TIMEOUT) {
- handleIqTimeoutResponse(response);
- }
- }
-
- private void handleIqErrorResponse(final IqPacket response) {
- Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
- final String errorCondition = response.getErrorCondition();
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": received IQ-error from "
- + response.getFrom()
- + " in RTP session. "
- + errorCondition);
- if (isTerminated()) {
- Log.i(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": ignoring error because session was already terminated");
- return;
- }
- this.webRTCWrapper.close();
- final State target;
- if (Arrays.asList(
- "service-unavailable",
- "recipient-unavailable",
- "remote-server-not-found",
- "remote-server-timeout")
- .contains(errorCondition)) {
- target = State.TERMINATED_CONNECTIVITY_ERROR;
- } else {
- target = State.TERMINATED_APPLICATION_FAILURE;
- }
- transitionOrThrow(target);
- this.finish();
- }
-
- private void handleIqTimeoutResponse(final IqPacket response) {
- Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": received IQ timeout in RTP session with "
- + id.with
- + ". terminating with connectivity error");
- if (isTerminated()) {
- Log.i(
- Config.LOGTAG,
- id.account.getJid().asBareJid()
- + ": ignoring error because session was already terminated");
- return;
- }
- this.webRTCWrapper.close();
- transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
- this.finish();
- }
-
- private void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
- Log.d(
- Config.LOGTAG,
- id.account.getJid().asBareJid() + ": terminating session with out-of-order");
- this.webRTCWrapper.close();
- transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
- respondWithOutOfOrder(jinglePacket);
- this.finish();
- }
-
- private void respondWithTieBreak(final JinglePacket jinglePacket) {
- respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
- }
-
- private void respondWithOutOfOrder(final JinglePacket jinglePacket) {
- respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
- }
-
- private void respondWithItemNotFound(final JinglePacket jinglePacket) {
- respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
- }
-
- void respondWithJingleError(
- final IqPacket original,
- String jingleCondition,
- String condition,
- String conditionType) {
- jingleConnectionManager.respondWithJingleError(
- id.account, original, jingleCondition, condition, conditionType);
- }
-
- private void respondOk(final JinglePacket jinglePacket) {
- xmppConnectionService.sendIqPacket(
- id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
- }
-
public RtpEndUserState getEndUserState() {
switch (this.state) {
case NULL, PROPOSED, SESSION_INITIALIZED -> {
@@ -2409,7 +2176,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ ": received endCall() when session has already been terminated. nothing to do");
return;
}
- if (isInState(State.PROPOSED) && !isInitiator()) {
+ if (isInState(State.PROPOSED) && isResponder()) {
rejectCallFromProposed();
return;
}
@@ -2538,22 +2305,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
sendSessionAccept();
}
- private synchronized boolean isInState(State... state) {
- return Arrays.asList(state).contains(this.state);
- }
-
- private boolean transition(final State target) {
- return transition(target, null);
- }
- private synchronized boolean transition(final State target, final Runnable runnable) {
- final Collection validTransitions = VALID_TRANSITIONS.get(this.state);
- if (validTransitions != null && validTransitions.contains(target)) {
- this.state = target;
- if (runnable != null) {
- runnable.run();
- }
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
+ @Override
+ protected synchronized boolean transition(final State target, final Runnable runnable) {
+ if (super.transition(target, runnable)) {
updateEndUserState();
updateOngoingCallNotification();
return true;
@@ -2562,13 +2317,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- void transitionOrThrow(final State target) {
- if (!transition(target)) {
- throw new IllegalStateException(
- String.format("Unable to transition from %s to %s", this.state, target));
- }
- }
-
@Override
public void onIceCandidate(final IceCandidate iceCandidate) {
final RtpContentMap rtpContentMap =
@@ -2904,98 +2652,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.account,
request,
(account, response) -> {
- ImmutableList.Builder listBuilder =
- new ImmutableList.Builder<>();
- if (response.getType() == IqPacket.TYPE.RESULT) {
- final Element services =
- response.findChild(
- "services", Namespace.EXTERNAL_SERVICE_DISCOVERY);
- final List 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 iceServers = listBuilder.build();
+ final var iceServers = IceServers.parse(response);
if (iceServers.size() == 0) {
Log.w(
Config.LOGTAG,
@@ -3012,13 +2669,19 @@ public class JingleRtpConnection extends AbstractJingleConnection
onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList());
}
}
+
+ @Override
+ protected void terminateTransport() {
+ this.webRTCWrapper.close();
+ }
- private void finish() {
+ @Override
+ protected void finish() {
if (isTerminated()) {
this.cancelRingingTimeout();
this.webRTCWrapper.verifyClosed();
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
- this.jingleConnectionManager.finishConnectionOrThrow(this);
+ super.finish();
try {
File log = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Cheogram/calls/" + id.getWith().asBareJid() + "." + id.getSessionId() + "." + created + ".log");
log.getParentFile().mkdirs();
@@ -3061,14 +2724,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
- public State getState() {
- return this.state;
- }
-
- boolean isTerminated() {
- return TERMINATED.contains(this.state);
- }
-
public Optional getLocalVideoTrack() {
return webRTCWrapper.getLocalVideoTrack();
}
@@ -3107,17 +2762,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
return remoteHasFeature(Namespace.SDP_OFFER_ANSWER);
}
- private boolean remoteHasFeature(final String feature) {
- final Contact contact = id.getContact();
- final Presence presence =
- contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
- final ServiceDiscoveryResult serviceDiscoveryResult =
- presence == null ? null : presence.getServiceDiscoveryResult();
- final List features =
- serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
- return features != null && features.contains(feature);
- }
-
private interface OnIceServersDiscovered {
void onIceServersDiscovered(List iceServers);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
deleted file mode 100644
index a57f4927ff8d65108d4563e14af7882eda8d5cc9..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
deleted file mode 100644
index e832d3f584044237e937c70016d2e6358c1f843a..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java
+++ /dev/null
@@ -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();
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java
index 67e275414652e1ff7791190584b2a6b23dfb6f89..db33666cb1fc4c2cd7f536650e40f2ba3e3085f9 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java
@@ -1,6 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Multimap;
import java.util.List;
@@ -8,9 +9,9 @@ public class MediaBuilder {
private String media;
private int port;
private String protocol;
- private List formats;
+ private String format;
private String connectionData;
- private ArrayListMultimap attributes;
+ private Multimap attributes;
public MediaBuilder setMedia(String media) {
this.media = media;
@@ -27,8 +28,13 @@ public class MediaBuilder {
return this;
}
- public MediaBuilder setFormats(List formats) {
- this.formats = formats;
+ public MediaBuilder setFormats(final List formats) {
+ this.format = Joiner.on(' ').join(formats);
+ return this;
+ }
+
+ public MediaBuilder setFormat(final String format) {
+ this.format = format;
return this;
}
@@ -37,12 +43,13 @@ public class MediaBuilder {
return this;
}
- public MediaBuilder setAttributes(ArrayListMultimap attributes) {
+ public MediaBuilder setAttributes(Multimap attributes) {
this.attributes = attributes;
return this;
}
public SessionDescription.Media createMedia() {
- return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes);
+ return new SessionDescription.Media(
+ media, port, protocol, format, connectionData, attributes);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java
index f5e041014c95a78841179074bd05cc35288eb53d..0d5d32d5089bf240a9814b904fa2f06d25e99792 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OmemoVerifiedRtpContentMap.java
@@ -3,12 +3,14 @@ package eu.siacs.conversations.xmpp.jingle;
import java.util.Map;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public class OmemoVerifiedRtpContentMap extends RtpContentMap {
- public OmemoVerifiedRtpContentMap(Group group, Map contents) {
+ public OmemoVerifiedRtpContentMap(Group group, Map> contents) {
super(group, contents);
- for(final DescriptionTransport descriptionTransport : contents.values()) {
+ for(final DescriptionTransport descriptionTransport : contents.values()) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport).ensureNoPlaintextFingerprint();
continue;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
deleted file mode 100644
index 76e337177e682c1aed2d06690ec14595b60641f6..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-public interface OnPrimaryCandidateFound {
- void onPrimaryCandidateFound(boolean success, JingleCandidate canditate);
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
index 2e548d60ada2d52cb996e228e5be3c175cf8e331..b151af17e02fc837a599433ce568e1b630d35dbf 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java
@@ -6,7 +6,6 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
@@ -32,19 +31,17 @@ import java.util.Set;
import javax.annotation.Nonnull;
-public class RtpContentMap {
+public class RtpContentMap extends AbstractContentMap {
- public final Group group;
- public final Map contents;
-
- public RtpContentMap(Group group, Map contents) {
- this.group = group;
- this.contents = contents;
+ public RtpContentMap(
+ Group group,
+ Map> contents) {
+ super(group, contents);
}
public static RtpContentMap of(final JinglePacket jinglePacket) {
- final Map contents =
- DescriptionTransport.of(jinglePacket.getJingleContents());
+ final Map> contents =
+ of(jinglePacket.getJingleContents());
if (isOmemoVerified(contents)) {
return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents);
} else {
@@ -52,12 +49,15 @@ public class RtpContentMap {
}
}
- private static boolean isOmemoVerified(Map contents) {
- final Collection values = contents.values();
+ private static boolean isOmemoVerified(
+ Map> contents) {
+ final Collection> values =
+ contents.values();
if (values.size() == 0) {
return false;
}
- for (final DescriptionTransport descriptionTransport : values) {
+ for (final DescriptionTransport descriptionTransport :
+ values) {
if (descriptionTransport.transport instanceof OmemoVerifiedIceUdpTransportInfo) {
continue;
}
@@ -68,13 +68,13 @@ public class RtpContentMap {
public static RtpContentMap of(
final SessionDescription sessionDescription, final boolean isInitiator) {
- final ImmutableMap.Builder contentMapBuilder =
- new ImmutableMap.Builder<>();
+ final ImmutableMap.Builder<
+ String, DescriptionTransport>
+ contentMapBuilder = new ImmutableMap.Builder<>();
for (SessionDescription.Media media : sessionDescription.media) {
final String id = Iterables.getFirst(media.attributes.get("mid"), null);
Preconditions.checkNotNull(id, "media has no mid");
- contentMapBuilder.put(
- id, DescriptionTransport.of(sessionDescription, isInitiator, media));
+ contentMapBuilder.put(id, of(sessionDescription, isInitiator, media));
}
final String groupAttribute =
Iterables.getFirst(sessionDescription.attributes.get("group"), null);
@@ -95,26 +95,6 @@ public class RtpContentMap {
}));
}
- public Set getSenders() {
- return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
- }
-
- public List getNames() {
- return ImmutableList.copyOf(contents.keySet());
- }
-
- void requireContentDescriptions() {
- if (this.contents.size() == 0) {
- throw new IllegalStateException("No contents available");
- }
- for (Map.Entry entry : this.contents.entrySet()) {
- if (entry.getValue().description == null) {
- throw new IllegalStateException(
- String.format("%s is lacking content description", entry.getKey()));
- }
- }
- }
-
void requireDTLSFingerprint() {
requireDTLSFingerprint(false);
}
@@ -123,7 +103,8 @@ public class RtpContentMap {
if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available");
}
- for (Map.Entry entry : this.contents.entrySet()) {
+ for (Map.Entry> entry :
+ this.contents.entrySet()) {
final IceUdpTransportInfo transport = entry.getValue().transport;
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint == null
@@ -147,31 +128,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 entry : this.contents.entrySet()) {
- final DescriptionTransport descriptionTransport = entry.getValue();
- final Content content =
- new Content(
- Content.Creator.INITIATOR,
- descriptionTransport.senders,
- entry.getKey());
- if (descriptionTransport.description != null) {
- content.addChild(descriptionTransport.description);
- }
- content.addChild(descriptionTransport.transport);
- jinglePacket.addJingleContent(content);
- }
- return jinglePacket;
- }
-
RtpContentMap transportInfo(
final String contentName, final IceUdpTransportInfo.Candidate candidate) {
- final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
+ final DescriptionTransport descriptionTransport =
+ contents.get(contentName);
final IceUdpTransportInfo transportInfo =
descriptionTransport == null ? null : descriptionTransport.transport;
if (transportInfo == null) {
@@ -184,7 +144,7 @@ public class RtpContentMap {
null,
ImmutableMap.of(
contentName,
- new DescriptionTransport(
+ new DescriptionTransport<>(
descriptionTransport.senders, null, newTransportInfo)));
}
@@ -194,21 +154,24 @@ public class RtpContentMap {
Maps.transformValues(
contents,
dt ->
- new DescriptionTransport(
+ new DescriptionTransport<>(
dt.senders, null, dt.transport.cloneWrapper())));
}
RtpContentMap withCandidates(
ImmutableMultimap candidates) {
- final ImmutableMap.Builder contentBuilder =
- new ImmutableMap.Builder<>();
- for (final Map.Entry entry : this.contents.entrySet()) {
+ final ImmutableMap.Builder<
+ String, DescriptionTransport>
+ contentBuilder = new ImmutableMap.Builder<>();
+ for (final Map.Entry>
+ entry : this.contents.entrySet()) {
final String name = entry.getKey();
- final DescriptionTransport descriptionTransport = entry.getValue();
+ final DescriptionTransport descriptionTransport =
+ entry.getValue();
final var transport = descriptionTransport.transport;
contentBuilder.put(
name,
- new DescriptionTransport(
+ new DescriptionTransport<>(
descriptionTransport.senders,
descriptionTransport.description,
transport.withCandidates(candidates.get(name))));
@@ -248,7 +211,7 @@ public class RtpContentMap {
}
public IceUdpTransportInfo.Credentials getCredentials(final String contentName) {
- final DescriptionTransport descriptionTransport = this.contents.get(contentName);
+ final var descriptionTransport = this.contents.get(contentName);
if (descriptionTransport == null) {
throw new IllegalArgumentException(
String.format(
@@ -288,7 +251,7 @@ public class RtpContentMap {
public boolean emptyCandidates() {
int count = 0;
- for (DescriptionTransport descriptionTransport : contents.values()) {
+ for (final var descriptionTransport : contents.values()) {
count += descriptionTransport.transport.getCandidates().size();
}
return count == 0;
@@ -301,17 +264,19 @@ public class RtpContentMap {
public RtpContentMap modifiedCredentials(
IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) {
- final ImmutableMap.Builder contentMapBuilder =
- new ImmutableMap.Builder<>();
- for (final Map.Entry content : contents.entrySet()) {
- final DescriptionTransport descriptionTransport = content.getValue();
+ final ImmutableMap.Builder<
+ String, DescriptionTransport>
+ contentMapBuilder = new ImmutableMap.Builder<>();
+ for (final Map.Entry>
+ content : contents.entrySet()) {
+ final var descriptionTransport = content.getValue();
final RtpDescription rtpDescription = descriptionTransport.description;
final IceUdpTransportInfo transportInfo = descriptionTransport.transport;
final IceUdpTransportInfo modifiedTransportInfo =
transportInfo.modifyCredentials(credentials, setup);
contentMapBuilder.put(
content.getKey(),
- new DescriptionTransport(
+ new DescriptionTransport<>(
descriptionTransport.senders, rtpDescription, modifiedTransportInfo));
}
return new RtpContentMap(this.group, contentMapBuilder.build());
@@ -322,16 +287,18 @@ public class RtpContentMap {
this.group,
Maps.transformValues(
contents,
- dt -> new DescriptionTransport(senders, dt.description, dt.transport)));
+ dt -> new DescriptionTransport<>(senders, dt.description, dt.transport)));
}
public RtpContentMap modifiedSendersChecked(
final boolean isInitiator, final Map modification) {
- final ImmutableMap.Builder contentMapBuilder =
- new ImmutableMap.Builder<>();
- for (final Map.Entry content : contents.entrySet()) {
+ final ImmutableMap.Builder<
+ String, DescriptionTransport>
+ contentMapBuilder = new ImmutableMap.Builder<>();
+ for (final Map.Entry>
+ content : contents.entrySet()) {
final String id = content.getKey();
- final DescriptionTransport descriptionTransport = content.getValue();
+ final var descriptionTransport = content.getValue();
final Content.Senders currentSenders = descriptionTransport.senders;
final Content.Senders targetSenders = modification.get(id);
if (targetSenders == null || currentSenders == targetSenders) {
@@ -340,7 +307,7 @@ public class RtpContentMap {
checkSenderModification(isInitiator, currentSenders, targetSenders);
contentMapBuilder.put(
id,
- new DescriptionTransport(
+ new DescriptionTransport<>(
targetSenders,
descriptionTransport.description,
descriptionTransport.transport));
@@ -387,7 +354,7 @@ public class RtpContentMap {
Maps.transformValues(
this.contents,
dt ->
- new DescriptionTransport(
+ new DescriptionTransport<>(
dt.senders,
RtpDescription.stub(dt.description.getMedia()),
IceUdpTransportInfo.STUB)));
@@ -416,120 +383,96 @@ public class RtpContentMap {
public RtpContentMap addContent(
final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) {
- final Map combined = merge(contents, modification.contents);
- final Map combinedFixedTransport =
- Maps.transformValues(
- combined,
- dt -> {
- final IceUdpTransportInfo iceUdpTransportInfo;
- if (dt.transport.isStub()) {
- final IceUdpTransportInfo.Credentials credentials =
- getDistinctCredentials();
- final Collection iceOptions = getCombinedIceOptions();
- final DTLS dtls = getDistinctDtls();
- iceUdpTransportInfo =
- IceUdpTransportInfo.of(
- credentials,
- iceOptions,
- setupOverwrite,
- dtls.hash,
- dtls.fingerprint);
- } else {
- final IceUdpTransportInfo.Fingerprint fp =
- dt.transport.getFingerprint();
- final IceUdpTransportInfo.Setup setup = fp.getSetup();
- iceUdpTransportInfo =
- IceUdpTransportInfo.of(
- dt.transport.getCredentials(),
- dt.transport.getIceOptions(),
- setup == IceUdpTransportInfo.Setup.ACTPASS
- ? setupOverwrite
- : setup,
- fp.getHash(),
- fp.getContent());
- }
- return new DescriptionTransport(
- dt.senders, dt.description, iceUdpTransportInfo);
- });
+ final Map> combined =
+ merge(contents, modification.contents);
+ final Map>
+ combinedFixedTransport =
+ Maps.transformValues(
+ combined,
+ dt -> {
+ final IceUdpTransportInfo iceUdpTransportInfo;
+ if (dt.transport.isStub()) {
+ final IceUdpTransportInfo.Credentials credentials =
+ getDistinctCredentials();
+ final Collection iceOptions =
+ getCombinedIceOptions();
+ final DTLS dtls = getDistinctDtls();
+ iceUdpTransportInfo =
+ IceUdpTransportInfo.of(
+ credentials,
+ iceOptions,
+ setupOverwrite,
+ dtls.hash,
+ dtls.fingerprint);
+ } else {
+ final IceUdpTransportInfo.Fingerprint fp =
+ dt.transport.getFingerprint();
+ final IceUdpTransportInfo.Setup setup = fp.getSetup();
+ iceUdpTransportInfo =
+ IceUdpTransportInfo.of(
+ dt.transport.getCredentials(),
+ dt.transport.getIceOptions(),
+ setup == IceUdpTransportInfo.Setup.ACTPASS
+ ? setupOverwrite
+ : setup,
+ fp.getHash(),
+ fp.getContent());
+ }
+ return new DescriptionTransport<>(
+ dt.senders, dt.description, iceUdpTransportInfo);
+ });
return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport));
}
- private static Map merge(
- final Map a, final Map b) {
- final Map combined = new LinkedHashMap<>();
+ private static Map> merge(
+ final Map> a,
+ final Map> b) {
+ final Map> combined =
+ new LinkedHashMap<>();
combined.putAll(a);
combined.putAll(b);
return ImmutableMap.copyOf(combined);
}
- public static class DescriptionTransport {
- public final Content.Senders senders;
- public final RtpDescription description;
- public final IceUdpTransportInfo transport;
-
- public DescriptionTransport(
- final Content.Senders senders,
- final RtpDescription description,
- final IceUdpTransportInfo transport) {
- this.senders = senders;
- this.description = description;
- this.transport = transport;
- }
-
- public static DescriptionTransport of(final Content content) {
- final GenericDescription description = content.getDescription();
- final GenericTransportInfo transportInfo = content.getTransport();
- final Content.Senders senders = content.getSenders();
- final RtpDescription rtpDescription;
- final IceUdpTransportInfo iceUdpTransportInfo;
- if (description == null) {
- rtpDescription = null;
- } else if (description instanceof RtpDescription) {
- rtpDescription = (RtpDescription) description;
- } else {
- throw new UnsupportedApplicationException(
- "Content does not contain rtp description");
- }
- if (transportInfo instanceof IceUdpTransportInfo) {
- iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
- } else {
- throw new UnsupportedTransportException(
- "Content does not contain ICE-UDP transport");
- }
- return new DescriptionTransport(
- senders,
- rtpDescription,
- OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
- }
-
- private static DescriptionTransport of(
- final SessionDescription sessionDescription,
- final boolean isInitiator,
- final SessionDescription.Media media) {
- final Content.Senders senders = Content.Senders.of(media, isInitiator);
- final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
- final IceUdpTransportInfo transportInfo =
- IceUdpTransportInfo.of(sessionDescription, media);
- return new DescriptionTransport(senders, rtpDescription, transportInfo);
+ public static DescriptionTransport of(
+ final Content content) {
+ final GenericDescription description = content.getDescription();
+ final GenericTransportInfo transportInfo = content.getTransport();
+ final Content.Senders senders = content.getSenders();
+ final RtpDescription rtpDescription;
+ final IceUdpTransportInfo iceUdpTransportInfo;
+ if (description == null) {
+ rtpDescription = null;
+ } else if (description instanceof RtpDescription) {
+ rtpDescription = (RtpDescription) description;
+ } else {
+ throw new UnsupportedApplicationException("Content does not contain rtp description");
}
-
- public static Map of(final Map contents) {
- return ImmutableMap.copyOf(
- Maps.transformValues(
- contents, content -> content == null ? null : of(content)));
+ if (transportInfo instanceof IceUdpTransportInfo) {
+ iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo;
+ } else {
+ throw new UnsupportedTransportException("Content does not contain ICE-UDP transport");
}
+ return new DescriptionTransport<>(
+ senders,
+ rtpDescription,
+ OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo));
}
- public static class UnsupportedApplicationException extends IllegalArgumentException {
- UnsupportedApplicationException(String message) {
- super(message);
- }
+ private static DescriptionTransport of(
+ final SessionDescription sessionDescription,
+ final boolean isInitiator,
+ final SessionDescription.Media media) {
+ final Content.Senders senders = Content.Senders.of(media, isInitiator);
+ final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media);
+ final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media);
+ return new DescriptionTransport<>(senders, rtpDescription, transportInfo);
}
- public static class UnsupportedTransportException extends IllegalArgumentException {
- UnsupportedTransportException(String message) {
- super(message);
- }
+ private static Map> of(
+ final Map contents) {
+ return ImmutableMap.copyOf(
+ Maps.transformValues(contents, content -> content == null ? null : of(content)));
}
public static final class Diff {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
index 2d2dc95709f22e86b5d88645bb14540205eaff12..025a7acc917fd0defb95953556b9b15f99cb271a 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java
@@ -10,12 +10,17 @@ import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
import java.util.Collection;
import java.util.Collections;
@@ -28,6 +33,8 @@ public class SessionDescription {
public static final String LINE_DIVIDER = "\r\n";
private static final String HARDCODED_MEDIA_PROTOCOL =
"UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint
+ private static final String HARDCODED_APPLICATION_PROTOCOL = "UDP/DTLS/SCTP";
+ private static final String FORMAT_WEBRTC_DATA_CHANNEL = "webrtc-datachannel";
private static final int HARDCODED_MEDIA_PORT = 9;
private static final Collection HARDCODED_ICE_OPTIONS =
Collections.singleton("trickle");
@@ -52,9 +59,8 @@ public class SessionDescription {
this.media = media;
}
- private static void appendAttributes(
- StringBuilder s, ArrayListMultimap attributes) {
- for (Map.Entry attribute : attributes.entries()) {
+ private static void appendAttributes(StringBuilder s, Multimap attributes) {
+ for (final Map.Entry attribute : attributes.entries()) {
final String key = attribute.getKey();
final String value = attribute.getValue();
s.append("a=").append(key);
@@ -79,24 +85,20 @@ public class SessionDescription {
final char key = pair[0].charAt(0);
final String value = pair[1];
switch (key) {
- case 'v':
- sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
- break;
- case 'c':
+ case 'v' -> sessionDescriptionBuilder.setVersion(ignorantIntParser(value));
+ case 'c' -> {
if (currentMediaBuilder != null) {
currentMediaBuilder.setConnectionData(value);
} else {
sessionDescriptionBuilder.setConnectionData(value);
}
- break;
- case 's':
- sessionDescriptionBuilder.setName(value);
- break;
- case 'a':
+ }
+ case 's' -> sessionDescriptionBuilder.setName(value);
+ case 'a' -> {
final Pair attribute = parseAttribute(value);
attributeMap.put(attribute.first, attribute.second);
- break;
- case 'm':
+ }
+ case 'm' -> {
if (currentMediaBuilder == null) {
sessionDescriptionBuilder.setAttributes(attributeMap);
} else {
@@ -118,7 +120,7 @@ public class SessionDescription {
} else {
Log.d(Config.LOGTAG, "skipping media line " + line);
}
- break;
+ }
}
}
if (currentMediaBuilder != null) {
@@ -131,6 +133,56 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription();
}
+ public static SessionDescription of(final FileTransferContentMap contentMap) {
+ final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
+ final ArrayListMultimap attributeMap = ArrayListMultimap.create();
+ final ImmutableList.Builder 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>
+ entry : contentMap.contents.entrySet()) {
+ final var dt = entry.getValue();
+ final WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo;
+ if (dt.transport instanceof WebRTCDataChannelTransportInfo transportInfo) {
+ webRTCDataChannelTransportInfo = transportInfo;
+ } else {
+ throw new IllegalArgumentException("Transport is not of type WebRTCDataChannel");
+ }
+ final String name = entry.getKey();
+ checkNoWhitespace(name, "content name must not contain any whitespace");
+
+ final MediaBuilder mediaBuilder = new MediaBuilder();
+ mediaBuilder.setMedia("application");
+ mediaBuilder.setConnectionData(HARDCODED_CONNECTION);
+ mediaBuilder.setPort(HARDCODED_MEDIA_PORT);
+ mediaBuilder.setProtocol(HARDCODED_APPLICATION_PROTOCOL);
+ mediaBuilder.setAttributes(
+ transportInfoMediaAttributes(webRTCDataChannelTransportInfo));
+ mediaBuilder.setFormat(FORMAT_WEBRTC_DATA_CHANNEL);
+ mediaListBuilder.add(mediaBuilder.createMedia());
+ }
+
+ sessionDescriptionBuilder.setVersion(0);
+ sessionDescriptionBuilder.setName("-");
+ sessionDescriptionBuilder.setMedia(mediaListBuilder.build());
+ sessionDescriptionBuilder.setAttributes(attributeMap);
+ return sessionDescriptionBuilder.createSessionDescription();
+ }
+
public static SessionDescription of(
final RtpContentMap contentMap, final boolean isInitiatorContentMap) {
final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
@@ -140,58 +192,27 @@ public class SessionDescription {
if (group != null) {
final String semantics = group.getSemantics();
checkNoWhitespace(semantics, "group semantics value must not contain any whitespace");
- attributeMap.put(
- "group",
- group.getSemantics()
- + " "
- + Joiner.on(' ').join(group.getIdentificationTags()));
+ final var idTags = group.getIdentificationTags();
+ for (final String content : idTags) {
+ checkNoWhitespace(content, "group content names must not contain any whitespace");
+ }
+ attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(idTags));
}
+ // TODO my-media-stream can be removed I think
attributeMap.put("msid-semantic", " WMS my-media-stream");
- for (final Map.Entry entry :
- contentMap.contents.entrySet()) {
+ for (final Map.Entry>
+ entry : contentMap.contents.entrySet()) {
final String name = entry.getKey();
- RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue();
- RtpDescription description = descriptionTransport.description;
- IceUdpTransportInfo transport = descriptionTransport.transport;
+ checkNoWhitespace(name, "content name must not contain any whitespace");
+ final DescriptionTransport descriptionTransport =
+ entry.getValue();
+ final RtpDescription description = descriptionTransport.description;
final ArrayListMultimap 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 negotiatedIceOptions = transport.getIceOptions();
- final Collection iceOptions =
- negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions;
- mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions));
- final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
- if (fingerprint != null) {
- final String hashFunction = fingerprint.getHash();
- final String hash = fingerprint.getContent();
- if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) {
- throw new IllegalArgumentException("DTLS-SRTP missing hash");
- }
- checkNoWhitespace(
- hashFunction, "DTLS-SRTP hash function must not contain whitespace");
- checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace");
- mediaAttributes.put("fingerprint", hashFunction + " " + hash);
- final IceUdpTransportInfo.Setup setup = fingerprint.getSetup();
- if (setup != null) {
- mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT));
- }
- }
+ mediaAttributes.putAll(transportInfoMediaAttributes(descriptionTransport.transport));
final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>();
- for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
+ for (final RtpDescription.PayloadType payloadType : description.getPayloadTypes()) {
final String id = payloadType.getId();
if (Strings.isNullOrEmpty(id)) {
throw new IllegalArgumentException("Payload type is missing id");
@@ -353,6 +374,69 @@ public class SessionDescription {
return sessionDescriptionBuilder.createSessionDescription();
}
+ private static Multimap transportInfoMediaAttributes(
+ final IceUdpTransportInfo transport) {
+ final ArrayListMultimap 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 negotiatedIceOptions = transport.getIceOptions();
+ final Collection 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 transportInfoMediaAttributes(
+ final WebRTCDataChannelTransportInfo transport) {
+ final ArrayListMultimap mediaAttributes = ArrayListMultimap.create();
+ final var iceUdpTransportInfo = transport.innerIceUdpTransportInfo();
+ if (iceUdpTransportInfo == null) {
+ throw new IllegalArgumentException(
+ "Transport element is missing inner ice-udp transport");
+ }
+ mediaAttributes.putAll(transportInfoMediaAttributes(iceUdpTransportInfo));
+ final Integer sctpPort = transport.getSctpPort();
+ if (sctpPort == null) {
+ throw new IllegalArgumentException(
+ "Transport element is missing required sctp-port attribute");
+ }
+ mediaAttributes.put("sctp-port", String.valueOf(sctpPort));
+ final Integer maxMessageSize = transport.getMaxMessageSize();
+ if (maxMessageSize == null) {
+ throw new IllegalArgumentException(
+ "Transport element is missing required max-message-size");
+ }
+ mediaAttributes.put("max-message-size", String.valueOf(maxMessageSize));
+ return ImmutableMultimap.copyOf(mediaAttributes);
+ }
+
public static String checkNoWhitespace(final String input, final String message) {
if (CharMatcher.whitespace().matchesAnyOf(input)) {
throw new IllegalArgumentException(message);
@@ -421,7 +505,7 @@ public class SessionDescription {
.append(' ')
.append(media.protocol)
.append(' ')
- .append(Joiner.on(' ').join(media.formats))
+ .append(media.format)
.append(LINE_DIVIDER);
s.append("c=").append(media.connectionData).append(LINE_DIVIDER);
appendAttributes(s, media.attributes);
@@ -433,21 +517,21 @@ public class SessionDescription {
public final String media;
public final int port;
public final String protocol;
- public final List formats;
+ public final String format;
public final String connectionData;
- public final ArrayListMultimap attributes;
+ public final Multimap attributes;
public Media(
String media,
int port,
String protocol,
- List formats,
+ String format,
String connectionData,
- ArrayListMultimap attributes) {
+ Multimap attributes) {
this.media = media;
this.port = port;
this.protocol = protocol;
- this.formats = formats;
+ this.format = format;
this.connectionData = connectionData;
this.attributes = attributes;
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
index 7356477eed1883551cccf2c6dd4ff2308be4b582..58f87fd924e790b8661c0671f711b4be23e2628f 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java
@@ -50,6 +50,7 @@ import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
@@ -429,7 +430,7 @@ public class WebRTCWrapper {
}
}
- private static PeerConnection.RTCConfiguration buildConfiguration(
+ public static PeerConnection.RTCConfiguration buildConfiguration(
final List iceServers, final boolean trickle) {
final PeerConnection.RTCConfiguration rtcConfig =
new PeerConnection.RTCConfiguration(iceServers);
@@ -481,11 +482,6 @@ public class WebRTCWrapper {
"setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is);
}
- public void resetPendingCandidates() {
- this.readyToReceivedIceCandidates.set(true);
- this.iceCandidates.clear();
- }
-
synchronized void close() {
final PeerConnection peerConnection = this.peerConnection;
final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
@@ -616,7 +612,8 @@ public class WebRTCWrapper {
throw new IllegalStateException("Local video track does not exist");
}
- synchronized ListenableFuture setLocalDescription(final boolean waitForCandidates) {
+ synchronized ListenableFuture setLocalDescription(
+ final boolean waitForCandidates) {
this.setIsReadyToReceiveIceCandidates(false);
return Futures.transformAsync(
getPeerConnectionFuture(),
@@ -630,16 +627,20 @@ public class WebRTCWrapper {
new SetSdpObserver() {
@Override
public void onSetSuccess() {
- final var delay =
- waitForCandidates
- ? Futures.catching(Futures.withTimeout(iceGatheringComplete, 2, TimeUnit.SECONDS, JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE), Exception.class, (Exception e) -> { return null; }, MoreExecutors.directExecutor())
- : Futures.immediateVoidFuture();
- final var delayedSessionDescription =
- Futures.transformAsync(
- delay,
- v -> getLocalDescriptionFuture(),
- MoreExecutors.directExecutor());
- future.setFuture(delayedSessionDescription);
+ if (waitForCandidates) {
+ final var delay = getIceGatheringCompleteOrTimeout();
+ final var delayedSessionDescription =
+ Futures.transformAsync(
+ delay,
+ v -> {
+ iceCandidates.clear();
+ return getLocalDescriptionFuture();
+ },
+ MoreExecutors.directExecutor());
+ future.setFuture(delayedSessionDescription);
+ } else {
+ future.setFuture(getLocalDescriptionFuture());
+ }
}
@Override
@@ -653,6 +654,23 @@ public class WebRTCWrapper {
MoreExecutors.directExecutor());
}
+ private ListenableFuture getIceGatheringCompleteOrTimeout() {
+ return Futures.catching(
+ Futures.withTimeout(
+ iceGatheringComplete,
+ 2,
+ TimeUnit.SECONDS,
+ JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE),
+ TimeoutException.class,
+ ex -> {
+ Log.d(
+ EXTENDED_LOGGING_TAG,
+ "timeout while waiting for ICE gathering to complete");
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
private ListenableFuture getLocalDescriptionFuture() {
return Futures.submit(
() -> {
@@ -801,7 +819,7 @@ public class WebRTCWrapper {
void onRenegotiationNeeded();
}
- private abstract static class SetSdpObserver implements SdpObserver {
+ public abstract static class SetSdpObserver implements SdpObserver {
@Override
public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
@@ -827,12 +845,12 @@ public class WebRTCWrapper {
public static class PeerConnectionNotInitialized extends IllegalStateException {
- private PeerConnectionNotInitialized() {
+ public PeerConnectionNotInitialized() {
super("initialize PeerConnection first");
}
}
- private static class FailureToSetDescriptionException extends IllegalArgumentException {
+ public static class FailureToSetDescriptionException extends IllegalArgumentException {
public FailureToSetDescriptionException(String message) {
super(message);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
index 0cca6527abf8b7c38cba68966efd331354efe478..fbde212a3bd2516685dc5bc41900055984903acd 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java
@@ -8,14 +8,14 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
-import java.util.Locale;
-import java.util.Set;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
+import java.util.Locale;
+import java.util.Set;
+
public class Content extends Element {
public Content(final Creator creator, final Senders senders, final String name) {
@@ -65,7 +65,7 @@ public class Content extends Element {
return null;
}
final String namespace = description.getNamespace();
- if (FileTransferDescription.NAMESPACES.contains(namespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
return FileTransferDescription.upgrade(description);
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
return RtpDescription.upgrade(description);
@@ -90,9 +90,11 @@ public class Content extends Element {
if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) {
return IbbTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) {
- return S5BTransportInfo.upgrade(transport);
+ return SocksByteStreamsTransportInfo.upgrade(transport);
} else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) {
return IceUdpTransportInfo.upgrade(transport);
+ } else if (Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL.equals(namespace)) {
+ return WebRTCDataChannelTransportInfo.upgrade(transport);
} else if (transport != null) {
return GenericTransportInfo.upgrade(transport);
} else {
@@ -100,7 +102,6 @@ public class Content extends Element {
}
}
-
public void setTransport(GenericTransportInfo transportInfo) {
this.addChild(transportInfo);
}
@@ -141,7 +142,7 @@ public class Content extends Element {
} else if (attributes.contains("recvonly")) {
return initiator ? RESPONDER : INITIATOR;
}
- Log.w(Config.LOGTAG,"assuming default value for senders");
+ Log.w(Config.LOGTAG, "assuming default value for senders");
// If none of the attributes "sendonly", "recvonly", "inactive", and "sendrecv" is
// present, "sendrecv" SHOULD be assumed as the default
// https://www.rfc-editor.org/rfc/rfc4566
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java
index 8e0f2ebadc0321cbd90f22c29d62739680803a8c..3878d98d94554a539280bc402cff9b2079df60ba 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java
@@ -1,89 +1,233 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
-import com.google.common.base.Preconditions;
+import android.util.Log;
-import java.util.Arrays;
-import java.util.List;
+import androidx.annotation.NonNull;
-import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.xml.Element;
+import com.google.common.base.CaseFormat;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.BaseEncoding;
+import com.google.common.primitives.Longs;
-public class FileTransferDescription extends GenericDescription {
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
- public static List NAMESPACES = Arrays.asList(
- Version.FT_3.namespace,
- Version.FT_4.namespace,
- Version.FT_5.namespace
- );
+import java.util.List;
+public class FileTransferDescription extends GenericDescription {
- private FileTransferDescription(String name, String namespace) {
- super(name, namespace);
+ private FileTransferDescription() {
+ super("description", Namespace.JINGLE_APPS_FILE_TRANSFER);
}
- public Version getVersion() {
- final String namespace = getNamespace();
- if (namespace.equals(Version.FT_3.namespace)) {
- return Version.FT_3;
- } else if (namespace.equals(Version.FT_4.namespace)) {
- return Version.FT_4;
- } else if (namespace.equals(Version.FT_5.namespace)) {
- return Version.FT_5;
- } else {
- throw new IllegalStateException("Unknown namespace");
+ public static FileTransferDescription of(final File fileDescription) {
+ final var description = new FileTransferDescription();
+ final var file = description.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ file.addChild("name").setContent(fileDescription.name);
+ file.addChild("size").setContent(Long.toString(fileDescription.size));
+ if (fileDescription.mediaType != null) {
+ file.addChild("mediaType").setContent(fileDescription.mediaType);
}
+ return description;
}
- public 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 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 hashes = findHashes(fileElement.getChildren());
+ return new File(size, name, mediaType, hashes);
}
- 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");
+ 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;
}
- fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize()));
- fileElement.addChild("name").setContent(file.getName());
- if (axolotlMessage != null) {
- fileElement.addChild(axolotlMessage.toElement());
+ 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()));
}
- return description;
+ 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 findHashes(final List elements) {
+ final ImmutableList.Builder hashes = new ImmutableList.Builder<>();
+ for (final Element child : elements) {
+ if ("hash".equals(child.getName()) && Namespace.HASHES.equals(child.getNamespace())) {
+ final Algorithm algorithm;
+ try {
+ algorithm = Algorithm.of(child.getAttribute("algo"));
+ } catch (final IllegalArgumentException e) {
+ continue;
+ }
+ final String content = child.getContent();
+ if (Strings.isNullOrEmpty(content)) {
+ continue;
+ }
+ if (BaseEncoding.base64().canDecode(content)) {
+ hashes.add(new Hash(BaseEncoding.base64().decode(content), algorithm));
+ }
+ }
+ }
+ return hashes.build();
}
public static FileTransferDescription upgrade(final Element element) {
- Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
- Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace");
- final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace());
+ Preconditions.checkArgument(
+ "description".equals(element.getName()),
+ "Name of provided element is not description");
+ Preconditions.checkArgument(
+ element.getNamespace().equals(Namespace.JINGLE_APPS_FILE_TRANSFER),
+ "Element does not match a file transfer namespace");
+ final FileTransferDescription description = new FileTransferDescription();
description.setAttributes(element.getAttributes());
description.setChildren(element.getChildren());
return description;
}
- public enum Version {
- FT_3("urn:xmpp:jingle:apps:file-transfer:3"),
- FT_4("urn:xmpp:jingle:apps:file-transfer:4"),
- FT_5("urn:xmpp:jingle:apps:file-transfer:5");
+ public static final class Checksum extends SessionInfo {
+ public final List hashes;
+
+ public Checksum(final String name, List hashes) {
+ super(name);
+ this.hashes = hashes;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("hashes", hashes).toString();
+ }
+
+ @Override
+ public Element asElement() {
+ final var checksum = new Element("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ checksum.setAttribute("name", name);
+ final var file = checksum.addChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ for (final Hash hash : hashes) {
+ final var element = file.addChild("hash", Namespace.HASHES);
+ element.setAttribute(
+ "algo",
+ CaseFormat.UPPER_UNDERSCORE.to(
+ CaseFormat.LOWER_HYPHEN, hash.algorithm.toString()));
+ element.setContent(BaseEncoding.base64().encode(hash.hash));
+ }
+ return checksum;
+ }
+ }
+
+ public static final class Received extends SessionInfo {
+
+ public Received(String name) {
+ super(name);
+ }
+
+ @Override
+ public Element asElement() {
+ final var element = new Element("received", Namespace.JINGLE_APPS_FILE_TRANSFER);
+ element.setAttribute("name", name);
+ return element;
+ }
+ }
+
+ public abstract static sealed class SessionInfo permits Checksum, Received {
- private final String namespace;
+ public final String name;
- Version(String namespace) {
- this.namespace = namespace;
+ protected SessionInfo(final String name) {
+ this.name = name;
}
- public String getNamespace() {
- return namespace;
+ public abstract Element asElement();
+ }
+
+ public static class File {
+ public final long size;
+ public final String name;
+ public final String mediaType;
+
+ public final List hashes;
+
+ public File(long size, String name, String mediaType, List 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));
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java
index a8db0d09f9ee37dde138c9915f4c30163eb6c62e..3bb3076a7d38e327e67b0f63df62089fd3a98f69 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java
@@ -8,6 +8,7 @@ public class GenericDescription extends Element {
GenericDescription(String name, final String namespace) {
super(name, namespace);
+ Preconditions.checkArgument("description".equals(name));
}
public static GenericDescription upgrade(final Element element) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java
index 2f7873a860fc2d23dcabc75251578c333fc27b01..17f0b4567ea858ee16a7613a101139b86540ca7d 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java
@@ -41,7 +41,7 @@ public class Group extends Element {
}
public static Group ofSdpString(final String input) {
- ImmutableList.Builder tagBuilder = new ImmutableList.Builder<>();
+ final ImmutableList.Builder tagBuilder = new ImmutableList.Builder<>();
final String[] parts = input.split(" ");
if (parts.length >= 2) {
final String semantics = parts[0];
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java
index 90fb32903f87c55c5709cfaaeefbb8a7582a2847..ddab9640f62d901ff3def8262bcbb1d72bb8178f 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java
@@ -1,6 +1,8 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Longs;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
@@ -23,16 +25,9 @@ public class IbbTransportInfo extends GenericTransportInfo {
return this.getAttribute("sid");
}
- public int getBlockSize() {
+ public Long getBlockSize() {
final String blockSize = this.getAttribute("block-size");
- if (blockSize == null) {
- return 0;
- }
- try {
- return Integer.parseInt(blockSize);
- } catch (NumberFormatException e) {
- return 0;
- }
+ return Strings.isNullOrEmpty(blockSize) ? null : Longs.tryParse(blockSize);
}
public static IbbTransportInfo upgrade(final Element element) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
index 8c8a9683d09f08bd1c0b666c0d59335a8b1250f2..c2afe72f625614e09d727952ee1b7d0cc625b5dc 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java
@@ -15,11 +15,13 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.SessionDescription;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import java.util.Arrays;
import java.util.Collection;
@@ -198,7 +200,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
}
}
- public static class Candidate extends Element {
+ public static class Candidate extends Element implements Transport.Candidate {
private Candidate() {
super("candidate");
@@ -399,7 +401,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
return fingerprint;
}
- private static Fingerprint of(ArrayListMultimap attributes) {
+ private static Fingerprint of(final Multimap attributes) {
final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null);
final String setup = Iterables.getFirst(attributes.get("setup"), null);
if (setup != null && fingerprint != null) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
index 0863b29df83e9c10b65939f6fe84a01d2cb51812..552046fb855e2106ed037997f9e2b09fab57b02c 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import com.google.common.base.CaseFormat;
@@ -7,13 +9,16 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
-import java.util.Map;
-
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import java.util.Map;
+
public class JinglePacket extends IqPacket {
private JinglePacket() {
@@ -36,7 +41,7 @@ public class JinglePacket extends IqPacket {
return jinglePacket;
}
- //TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
+ // TODO deprecate this somehow and make file transfer fail if there are multiple (or something)
public Content getJingleContent() {
final Element content = getJingleChild("content");
return content == null ? null : Content.upgrade(content);
@@ -64,7 +69,7 @@ public class JinglePacket extends IqPacket {
return builder.build();
}
- public void addJingleContent(final Content content) { //take content interface
+ public void addJingleContent(final Content content) { // take content interface
addJingleChild(content);
}
@@ -94,13 +99,13 @@ public class JinglePacket extends IqPacket {
}
}
- //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
+ // RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise
public void setInitiator(final Jid initiator) {
Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID");
findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator);
}
- //RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
+ // RECOMMENDED for session-accept, NOT RECOMMENDED otherwise
public void setResponder(Jid responder) {
Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID");
findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder);
@@ -116,6 +121,39 @@ public class JinglePacket extends IqPacket {
jingle.addChild(child);
}
+ public void setSecurity(final String name, final XmppAxolotlMessage xmppAxolotlMessage) {
+ final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
+ security.setAttribute("name", name);
+ security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding");
+ security.setAttribute("type", AxolotlService.PEP_PREFIX);
+ security.addChild(xmppAxolotlMessage.toElement());
+ addJingleChild(security);
+ }
+
+ public XmppAxolotlMessage getSecurity(final String nameNeedle) {
+ final Element jingle = findChild("jingle", Namespace.JINGLE);
+ if (jingle == null) {
+ return null;
+ }
+ for (final Element child : jingle.getChildren()) {
+ if ("security".equals(child.getName())
+ && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) {
+ final String name = child.getAttribute("name");
+ final String type = child.getAttribute("type");
+ final String cipher = child.getAttribute("cipher");
+ if (nameNeedle.equals(name)
+ && AxolotlService.PEP_PREFIX.equals(type)
+ && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) {
+ final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX);
+ if (encrypted != null) {
+ return XmppAxolotlMessage.fromElement(encrypted, getFrom().asBareJid());
+ }
+ }
+ }
+ }
+ return null;
+ }
+
public String getSessionId() {
return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
}
@@ -142,7 +180,7 @@ public class JinglePacket extends IqPacket {
TRANSPORT_REPLACE;
public static Action of(final String value) {
- //TODO handle invalid
+ // TODO handle invalid
return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value));
}
@@ -153,7 +191,6 @@ public class JinglePacket extends IqPacket {
}
}
-
public static class ReasonWrapper {
public final Reason reason;
public final String text;
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java
index f72162be8e5896e22184ee41f699c1caafa0e13a..5236e7bc9fd4033f516e4775f42f9b3c2b504098 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java
@@ -18,7 +18,7 @@ public class Propose extends Element {
for (final Element child : getChildren()) {
if ("description".equals(child.getName())) {
final String namespace = child.getNamespace();
- if (FileTransferDescription.NAMESPACES.contains(namespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(namespace)) {
builder.add(FileTransferDescription.upgrade(child));
} else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
builder.add(RtpDescription.upgrade(child));
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java
deleted file mode 100644
index 8f8f13416e207003e67c8a0961ea07936bf68724..0000000000000000000000000000000000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java
+++ /dev/null
@@ -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 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 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;
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa5cee1b16bca82a192446020d909f948a0aedb4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/SocksByteStreamsTransportInfo.java
@@ -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 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 getCandidates() {
+ final ImmutableList.Builder candidateBuilder =
+ new ImmutableList.Builder<>();
+ for (final Element child : getChildren()) {
+ 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 {}
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..88c4f9f000f72d8fc7e461d28608c98098070907
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/WebRTCDataChannelTransportInfo.java
@@ -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 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();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce2d4b31f2a61298ca733bcb1fd1abff523fc73e
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java
@@ -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 asTransportInfo() {
+ return Futures.immediateFuture(
+ new TransportInfo(new IbbTransportInfo(streamId, blockSize), null));
+ }
+
+ @Override
+ public ListenableFuture 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
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbda1c62271125a9d0af2753c724c903aa05d5ec
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java
@@ -0,0 +1,901 @@
+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.ExecutionException;
+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 theirCandidates;
+ private final String theirDestination;
+ private final SettableFuture selectedByThemCandidate = SettableFuture.create();
+ private final SettableFuture theirProxyActivation = SettableFuture.create();
+
+ private final CountDownLatch terminationLatch = new CountDownLatch(1);
+
+ private final ConnectionProvider connectionProvider;
+ private final ListenableFuture 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 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, selectedByThemCandidate, 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 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 activateProxy(final Candidate candidate) {
+ Log.d(Config.LOGTAG, "trying to activate our proxy " + candidate);
+ final SettableFuture