implement see-other-host stream error + use aggressive reconnects for see-other-host + Upgrade minidns, verify DNSSEC when possible, show in UI

This commit is contained in:
Stephen Paul Weber 2023-11-07 06:03:09 +01:00 committed by Arne
parent 155101284c
commit 6335fe4bd2
22 changed files with 346 additions and 858 deletions

View file

@ -70,7 +70,7 @@ dependencies {
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
implementation 'org.gnu.inet:libidn:1.15'
implementation 'com.google.zxing:core:3.5.0'
implementation 'de.measite.minidns:minidns-hla:0.2.4'
implementation 'org.minidns:minidns-hla:1.0.4'
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
implementation 'org.whispersystems:signal-protocol-android:2.6.2'
implementation 'com.makeramen:roundedimageview:2.3.0'

View file

@ -1,221 +0,0 @@
package de.gultsch.minidns;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.os.Build;
import android.util.Log;
import androidx.collection.LruCache;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import de.measite.minidns.AbstractDNSClient;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Record;
import de.measite.minidns.record.Data;
import eu.siacs.conversations.Config;
import java.io.IOException;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
public class AndroidDNSClient extends AbstractDNSClient {
private static final long DNS_MAX_TTL = 86_400L;
private static final LruCache<QuestionServerTuple, DNSMessage> QUERY_CACHE =
new LruCache<>(1024);
private final Context context;
private final NetworkDataSource networkDataSource = new NetworkDataSource();
private boolean askForDnssec = false;
public AndroidDNSClient(final Context context) {
super();
this.setDataSource(networkDataSource);
this.context = context;
}
private static String getPrivateDnsServerName(final LinkProperties linkProperties) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return linkProperties.getPrivateDnsServerName();
} else {
return null;
}
}
private static boolean isPrivateDnsActive(final LinkProperties linkProperties) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return linkProperties.isPrivateDnsActive();
} else {
return false;
}
}
@Override
protected DNSMessage.Builder newQuestion(final DNSMessage.Builder message) {
message.setRecursionDesired(true);
message.getEdnsBuilder()
.setUdpPayloadSize(networkDataSource.getUdpPayloadSize())
.setDnssecOk(askForDnssec);
return message;
}
@Override
protected DNSMessage query(final DNSMessage.Builder queryBuilder) throws IOException {
final DNSMessage question = newQuestion(queryBuilder).build();
for (final DNSServer dnsServer : getDNSServers()) {
final QuestionServerTuple cacheKey = new QuestionServerTuple(dnsServer, question);
final DNSMessage cachedResponse = queryCache(cacheKey);
if (cachedResponse != null) {
return cachedResponse;
}
final DNSMessage response = this.networkDataSource.query(question, dnsServer);
if (response == null) {
continue;
}
switch (response.responseCode) {
case NO_ERROR:
case NX_DOMAIN:
break;
default:
continue;
}
cacheQuery(cacheKey, response);
return response;
}
return null;
}
public boolean isAskForDnssec() {
return askForDnssec;
}
public void setAskForDnssec(boolean askForDnssec) {
this.askForDnssec = askForDnssec;
}
private List<DNSServer> getDNSServers() {
final ImmutableList.Builder<DNSServer> dnsServerBuilder = new ImmutableList.Builder<>();
final ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final Network[] networks = getActiveNetworks(connectivityManager);
for (final Network network : networks) {
final LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
if (linkProperties == null) {
continue;
}
final String privateDnsServerName = getPrivateDnsServerName(linkProperties);
if (Strings.isNullOrEmpty(privateDnsServerName)) {
final boolean isPrivateDns = isPrivateDnsActive(linkProperties);
for (final InetAddress dnsServer : linkProperties.getDnsServers()) {
if (isPrivateDns) {
dnsServerBuilder.add(new DNSServer(dnsServer, Transport.TLS));
} else {
dnsServerBuilder.add(new DNSServer(dnsServer));
}
}
} else {
dnsServerBuilder.add(new DNSServer(privateDnsServerName, Transport.TLS));
}
}
return dnsServerBuilder.build();
}
private Network[] getActiveNetworks(final ConnectivityManager connectivityManager) {
if (connectivityManager == null) {
return new Network[0];
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
final Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork != null) {
return new Network[] {activeNetwork};
}
}
return connectivityManager.getAllNetworks();
}
private DNSMessage queryCache(final QuestionServerTuple key) {
final DNSMessage cachedResponse;
synchronized (QUERY_CACHE) {
cachedResponse = QUERY_CACHE.get(key);
if (cachedResponse == null) {
return null;
}
final long expiresIn = expiresIn(cachedResponse);
if (expiresIn < 0) {
QUERY_CACHE.remove(key);
return null;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.d(
Config.LOGTAG,
"DNS query came from cache. expires in " + Duration.ofMillis(expiresIn));
}
}
return cachedResponse;
}
private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) {
if (response.receiveTimestamp <= 0) {
return;
}
synchronized (QUERY_CACHE) {
QUERY_CACHE.put(key, response);
}
}
private static long ttl(final DNSMessage dnsMessage) {
final List<Record<? extends Data>> answerSection = dnsMessage.answerSection;
if (answerSection == null || answerSection.isEmpty()) {
final List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
if (authoritySection == null || authoritySection.isEmpty()) {
return 0;
} else {
return Collections.min(Collections2.transform(authoritySection, d -> d.ttl));
}
} else {
return Collections.min(Collections2.transform(answerSection, d -> d.ttl));
}
}
private static long expiresAt(final DNSMessage dnsMessage) {
return dnsMessage.receiveTimestamp + (Math.min(DNS_MAX_TTL, ttl(dnsMessage)) * 1000L);
}
private static long expiresIn(final DNSMessage dnsMessage) {
return expiresAt(dnsMessage) - System.currentTimeMillis();
}
private static class QuestionServerTuple {
private final DNSServer dnsServer;
private final DNSMessage question;
private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) {
this.dnsServer = dnsServer;
this.question = question.asNormalizedVersion();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QuestionServerTuple that = (QuestionServerTuple) o;
return Objects.equal(dnsServer, that.dnsServer)
&& Objects.equal(question, that.question);
}
@Override
public int hashCode() {
return Objects.hashCode(dnsServer, question);
}
}
}

View file

@ -1,104 +0,0 @@
package de.gultsch.minidns;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public final class DNSServer {
public final InetAddress inetAddress;
public final String hostname;
public final int port;
public final List<Transport> transports;
public DNSServer(InetAddress inetAddress, Integer port, Transport transport) {
this.inetAddress = inetAddress;
this.port = port == null ? 0 : port;
this.transports = Collections.singletonList(transport);
this.hostname = null;
}
public DNSServer(final String hostname, final Integer port, final Transport transport) {
Preconditions.checkArgument(
Arrays.asList(Transport.HTTPS, Transport.TLS).contains(transport),
"hostname validation only works with TLS based transports");
this.hostname = hostname;
this.port = port == null ? 0 : port;
this.transports = Collections.singletonList(transport);
this.inetAddress = null;
}
public DNSServer(final String hostname, final Transport transport) {
this(hostname, Transport.DEFAULT_PORTS.get(transport), transport);
}
public DNSServer(InetAddress inetAddress, Transport transport) {
this(inetAddress, Transport.DEFAULT_PORTS.get(transport), transport);
}
public DNSServer(final InetAddress inetAddress) {
this(inetAddress, 53, Arrays.asList(Transport.UDP, Transport.TCP));
}
public DNSServer(final InetAddress inetAddress, int port, List<Transport> transports) {
this(inetAddress, null, port, transports);
}
private DNSServer(
final InetAddress inetAddress,
final String hostname,
final int port,
final List<Transport> transports) {
this.inetAddress = inetAddress;
this.hostname = hostname;
this.port = port;
this.transports = transports;
}
public Transport uniqueTransport() {
return Iterables.getOnlyElement(this.transports);
}
public DNSServer asUniqueTransport(final Transport transport) {
Preconditions.checkArgument(
this.transports.contains(transport),
"This DNS server does not have transport ",
transport);
return new DNSServer(inetAddress, hostname, port, Collections.singletonList(transport));
}
@Override
@Nonnull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("inetAddress", inetAddress)
.add("hostname", hostname)
.add("port", port)
.add("transports", transports)
.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DNSServer dnsServer = (DNSServer) o;
return port == dnsServer.port
&& Objects.equal(inetAddress, dnsServer.inetAddress)
&& Objects.equal(hostname, dnsServer.hostname)
&& Objects.equal(transports, dnsServer.transports);
}
@Override
public int hashCode() {
return Objects.hashCode(inetAddress, hostname, port, transports);
}
}

View file

@ -1,199 +0,0 @@
package de.gultsch.minidns;
import android.util.Log;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import de.measite.minidns.DNSMessage;
import eu.siacs.conversations.Config;
import org.conscrypt.OkHostnameVerifier;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
final class DNSSocket implements Closeable {
public static final int QUERY_TIMEOUT = 5_000;
private final Semaphore semaphore = new Semaphore(1);
private final Map<Integer, SettableFuture<DNSMessage>> inFlightQueries = new HashMap<>();
private final Socket socket;
private final DataInputStream dataInputStream;
private final DataOutputStream dataOutputStream;
private DNSSocket(
final Socket socket,
final DataInputStream dataInputStream,
final DataOutputStream dataOutputStream) {
this.socket = socket;
this.dataInputStream = dataInputStream;
this.dataOutputStream = dataOutputStream;
new Thread(this::readDNSMessages).start();
}
private void readDNSMessages() {
try {
while (socket.isConnected()) {
final DNSMessage response = readDNSMessage();
final SettableFuture<DNSMessage> future;
synchronized (inFlightQueries) {
future = inFlightQueries.remove(response.id);
}
if (future != null) {
future.set(response);
} else {
Log.e(Config.LOGTAG, "no in flight query found for response id " + response.id);
}
}
evictInFlightQueries(new EOFException());
} catch (final IOException e) {
evictInFlightQueries(e);
}
}
private void evictInFlightQueries(final Exception e) {
synchronized (inFlightQueries) {
final Iterator<Map.Entry<Integer, SettableFuture<DNSMessage>>> iterator =
inFlightQueries.entrySet().iterator();
while (iterator.hasNext()) {
final Map.Entry<Integer, SettableFuture<DNSMessage>> entry = iterator.next();
entry.getValue().setException(e);
iterator.remove();
}
}
}
private static DNSSocket of(final Socket socket) throws IOException {
final DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
return new DNSSocket(socket, dataInputStream, dataOutputStream);
}
public static DNSSocket connect(final DNSServer dnsServer) throws IOException {
switch (dnsServer.uniqueTransport()) {
case TCP:
return connectTcpSocket(dnsServer);
case TLS:
return connectTlsSocket(dnsServer);
default:
throw new IllegalStateException("This is not a socket based transport");
}
}
private static DNSSocket connectTcpSocket(final DNSServer dnsServer) throws IOException {
Preconditions.checkArgument(dnsServer.uniqueTransport() == Transport.TCP);
final SocketAddress socketAddress =
new InetSocketAddress(dnsServer.inetAddress, dnsServer.port);
final Socket socket = new Socket();
socket.connect(socketAddress, QUERY_TIMEOUT / 2);
socket.setSoTimeout(QUERY_TIMEOUT);
return DNSSocket.of(socket);
}
private static DNSSocket connectTlsSocket(final DNSServer dnsServer) throws IOException {
Preconditions.checkArgument(dnsServer.uniqueTransport() == Transport.TLS);
final SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
final SSLSocket sslSocket = (SSLSocket) factory.createSocket();
if (Strings.isNullOrEmpty(dnsServer.hostname)) {
final SocketAddress socketAddress =
new InetSocketAddress(dnsServer.inetAddress, dnsServer.port);
sslSocket.connect(socketAddress, QUERY_TIMEOUT / 2);
sslSocket.setSoTimeout(QUERY_TIMEOUT);
sslSocket.startHandshake();
} else {
final SocketAddress socketAddress = new InetSocketAddress(dnsServer.hostname, dnsServer.port);
sslSocket.connect(socketAddress, QUERY_TIMEOUT / 2);
sslSocket.setSoTimeout(QUERY_TIMEOUT);
sslSocket.startHandshake();
final SSLSession session = sslSocket.getSession();
final Certificate[] peerCertificates = session.getPeerCertificates();
if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate)) {
throw new IOException("Peer did not provide X509 certificates");
}
final X509Certificate certificate = (X509Certificate) peerCertificates[0];
if (!OkHostnameVerifier.strictInstance().verify(dnsServer.hostname, certificate)) {
throw new SSLPeerUnverifiedException("Peer did not provide valid certificates");
}
}
return DNSSocket.of(sslSocket);
}
public DNSMessage query(final DNSMessage query) throws IOException, InterruptedException {
try {
return queryAsync(query).get(QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (final ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new IOException(e);
}
} catch (final TimeoutException e) {
throw new IOException(e);
}
}
public ListenableFuture<DNSMessage> queryAsync(final DNSMessage query)
throws InterruptedException, IOException {
final SettableFuture<DNSMessage> responseFuture = SettableFuture.create();
synchronized (this.inFlightQueries) {
this.inFlightQueries.put(query.id, responseFuture);
}
this.semaphore.acquire();
try {
query.writeTo(this.dataOutputStream);
this.dataOutputStream.flush();
} finally {
this.semaphore.release();
}
return responseFuture;
}
private DNSMessage readDNSMessage() throws IOException {
final int length = this.dataInputStream.readUnsignedShort();
byte[] data = new byte[length];
int read = 0;
while (read < length) {
read += this.dataInputStream.read(data, read, length - read);
}
return new DNSMessage(data);
}
@Override
public void close() throws IOException {
this.socket.close();
}
public void closeQuietly() {
try {
this.socket.close();
} catch (final IOException ignored) {
}
}
}

View file

@ -1,160 +0,0 @@
package de.gultsch.minidns;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.collect.ImmutableList;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.MiniDNSException;
import de.measite.minidns.source.DNSDataSource;
import de.measite.minidns.util.MultipleIoException;
import eu.siacs.conversations.Config;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class NetworkDataSource extends DNSDataSource {
private static final LoadingCache<DNSServer, DNSSocket> socketCache =
CacheBuilder.newBuilder()
.removalListener(
(RemovalListener<DNSServer, DNSSocket>)
notification -> {
final DNSServer dnsServer = notification.getKey();
final DNSSocket dnsSocket = notification.getValue();
if (dnsSocket == null) {
return;
}
Log.d(Config.LOGTAG, "closing connection to " + dnsServer);
dnsSocket.closeQuietly();
})
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(
new CacheLoader<DNSServer, DNSSocket>() {
@Override
@NonNull
public DNSSocket load(@NonNull final DNSServer dnsServer)
throws Exception {
Log.d(Config.LOGTAG, "establishing connection to " + dnsServer);
return DNSSocket.connect(dnsServer);
}
});
private static List<Transport> transportsForPort(final int port) {
final ImmutableList.Builder<Transport> transportBuilder = new ImmutableList.Builder<>();
for (final Map.Entry<Transport, Integer> entry : Transport.DEFAULT_PORTS.entrySet()) {
if (entry.getValue().equals(port)) {
transportBuilder.add(entry.getKey());
}
}
return transportBuilder.build();
}
@Override
public DNSMessage query(final DNSMessage message, final InetAddress address, final int port)
throws IOException {
final List<Transport> transports = transportsForPort(port);
Log.w(
Config.LOGTAG,
"using legacy DataSource interface. guessing transports "
+ transports
+ " from port");
if (transports.isEmpty()) {
throw new IOException(String.format("No transports found for port %d", port));
}
return query(message, new DNSServer(address, port, transports));
}
public DNSMessage query(final DNSMessage message, final DNSServer dnsServer)
throws IOException {
Log.d(Config.LOGTAG, "using " + dnsServer);
final List<IOException> ioExceptions = new ArrayList<>();
for (final Transport transport : dnsServer.transports) {
try {
final DNSMessage response =
queryWithUniqueTransport(message, dnsServer.asUniqueTransport(transport));
if (response != null && !response.truncated) {
return response;
}
} catch (final IOException e) {
ioExceptions.add(e);
} catch (final InterruptedException e) {
throw new IOException(e);
}
}
MultipleIoException.throwIfRequired(ioExceptions);
return null;
}
private DNSMessage queryWithUniqueTransport(final DNSMessage message, final DNSServer dnsServer)
throws IOException, InterruptedException {
final Transport transport = dnsServer.uniqueTransport();
switch (transport) {
case UDP:
return queryUdp(message, dnsServer.inetAddress, dnsServer.port);
case TCP:
case TLS:
return queryDnsSocket(message, dnsServer);
default:
throw new IOException(
String.format("Transport %s has not been implemented", transport));
}
}
protected DNSMessage queryUdp(
final DNSMessage message, final InetAddress address, final int port)
throws IOException {
final DatagramPacket request = message.asDatagram(address, port);
final byte[] buffer = new byte[udpPayloadSize];
try (final DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(timeout);
socket.send(request);
final DatagramPacket response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
DNSMessage dnsMessage = new DNSMessage(response.getData());
if (dnsMessage.id != message.id) {
throw new MiniDNSException.IdMismatch(message, dnsMessage);
}
return dnsMessage;
}
}
protected DNSMessage queryDnsSocket(final DNSMessage message, final DNSServer dnsServer)
throws IOException, InterruptedException {
final DNSSocket cachedDnsSocket = socketCache.getIfPresent(dnsServer);
if (cachedDnsSocket != null) {
try {
return cachedDnsSocket.query(message);
} catch (final IOException e) {
Log.d(
Config.LOGTAG,
"IOException occurred at cached socket. invalidating and falling through to new socket creation");
socketCache.invalidate(dnsServer);
}
}
try {
return socketCache.get(dnsServer).query(message);
} catch (final ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else {
throw new IOException(cause);
}
}
}
}

View file

@ -1,23 +0,0 @@
package de.gultsch.minidns;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
public enum Transport {
UDP,
TCP,
TLS,
HTTPS;
public static final Map<Transport, Integer> DEFAULT_PORTS;
static {
final ImmutableMap.Builder<Transport, Integer> builder = new ImmutableMap.Builder<>();
builder.put(Transport.UDP, 53);
builder.put(Transport.TCP, 53);
builder.put(Transport.TLS, 853);
builder.put(Transport.HTTPS, 443);
DEFAULT_PORTS = builder.build();
}
}

View file

@ -884,6 +884,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
BIND_FAILURE,
HOST_UNKNOWN,
STREAM_ERROR,
SEE_OTHER_HOST,
STREAM_OPENING_ERROR,
POLICY_VIOLATION,
PAYMENT_REQUIRED,
@ -975,6 +976,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return R.string.account_status_stream_opening_error;
case PAYMENT_REQUIRED:
return R.string.payment_required;
case SEE_OTHER_HOST:
return R.string.reconnect_on_other_host;
case MISSING_INTERNET_PERMISSION:
return R.string.missing_internet_permission;
default:

View file

@ -550,7 +550,8 @@ public class XmppConnectionService extends Service {
} else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) {
resetSendingToWaiting(account);
if (connection != null && account.getStatus().isAttemptReconnect()) {
final boolean aggressive = hasJingleRtpConnection(account);
final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST
|| hasJingleRtpConnection(account);
final int next = connection.getTimeToNextAttempt(aggressive);
final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
if (next <= 0) {
@ -560,6 +561,13 @@ public class XmppConnectionService extends Service {
final int attempt = connection.getAttempt() + 1;
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode+", aggressive="+aggressive);
scheduleWakeUpCall(next, account.getUuid().hashCode());
if (aggressive) {
internalPingExecutor.schedule(
XmppConnectionService.this::manageAccountConnectionStatesInternal,
(next * 1000L) + 50,
TimeUnit.MILLISECONDS
);
}
}
}
}

View file

@ -1507,6 +1507,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} else {
this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
}
this.binding.verificationBox.setVisibility(View.VISIBLE);
if (mAccount.getXmppConnection() != null && mAccount.getXmppConnection().resolverAuthenticated()) {
this.binding.verificationMessage.setText("DNSSEC Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield);
} else {
this.binding.verificationMessage.setText("Not DNSSEC Verified");
this.binding.verificationIndicator.setImageResource(R.drawable.shield_question);
}
} else {
final TextInputLayout errorLayout;
if (this.mAccount.errorStatus()) {
@ -1529,6 +1537,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
removeErrorsOnAllBut(errorLayout);
this.binding.stats.setVisibility(View.GONE);
this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
this.binding.verificationBox.setVisibility(View.GONE);
}
}

View file

@ -73,6 +73,11 @@ public class AccountAdapter extends ArrayAdapter<Account> {
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError));
break;
}
if (account.getXmppConnection() != null && account.getXmppConnection().resolverAuthenticated()) {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield);
} else {
viewHolder.binding.verificationIndicator.setImageResource(R.drawable.shield_question);
}
final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener(null);
viewHolder.binding.tglAccountStatus.setChecked(!isDisabled);

View file

@ -8,9 +8,9 @@
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package eu.siacs.conversations.utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -19,17 +19,20 @@ import java.net.InetAddress;
import java.util.HashSet;
import java.util.logging.Level;
import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism;
import de.measite.minidns.dnsserverlookup.AndroidUsingReflection;
import de.measite.minidns.dnsserverlookup.DNSServerLookupMechanism;
import de.measite.minidns.util.PlatformDetection;
import java.util.ArrayList;
import java.util.List;
import org.minidns.dnsserverlookup.AbstractDnsServerLookupMechanism;
import org.minidns.dnsserverlookup.AndroidUsingReflection;
import org.minidns.dnsserverlookup.DnsServerLookupMechanism;
import org.minidns.util.PlatformDetection;
/**
* Try to retrieve the list of DNS server by executing getprop.
*/
public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanism {
public class AndroidUsingExecLowPriority extends AbstractDnsServerLookupMechanism {
public static final DNSServerLookupMechanism INSTANCE = new AndroidUsingExecLowPriority();
public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingExecLowPriority();
public static final int PRIORITY = AndroidUsingReflection.PRIORITY + 1;
private AndroidUsingExecLowPriority() {
@ -37,7 +40,7 @@ public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanis
}
@Override
public String[] getDnsServerAddresses() {
public List<String> getDnsServerAddresses() {
try {
Process process = Runtime.getRuntime().exec("getprop");
InputStream inputStream = process.getInputStream();
@ -76,7 +79,7 @@ public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanis
}
}
if (server.size() > 0) {
return server.toArray(new String[server.size()]);
return new ArrayList<>(server);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);

View file

@ -14,10 +14,10 @@ import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism;
import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
import org.minidns.dnsserverlookup.AbstractDnsServerLookupMechanism;
import org.minidns.dnsserverlookup.AndroidUsingExec;
public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism {
public class AndroidUsingLinkProperties extends AbstractDnsServerLookupMechanism {
private final Context context;
@ -33,11 +33,11 @@ public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism
@Override
@TargetApi(21)
public String[] getDnsServerAddresses() {
public List<String> getDnsServerAddresses() {
final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
final Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks();
if (networks == null) {
return new String[0];
return new ArrayList<>();
}
final Network activeNetwork = getActiveNetwork(connectivityManager);
final List<String> servers = new ArrayList<>();
@ -58,12 +58,12 @@ public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism
servers.addAll(vpnOffset, getIPv4First(linkProperties.getDnsServers()));
}
}
return servers.toArray(new String[0]);
return servers;
}
@TargetApi(23)
private static Network getActiveNetwork(ConnectivityManager cm) {
return cm.getActiveNetwork();
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? cm.getActiveNetwork() : null;
}
private static List<String> getIPv4First(List<InetAddress> in) {

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
import com.google.common.net.InetAddresses;
import java.util.regex.Pattern;
public class IP {
@ -27,4 +29,14 @@ public class IP {
}
}
public static String unwrapIPv6(final String host) {
if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') {
final String ip = host.substring(1,host.length() -1);
if (InetAddresses.isInetAddress(ip)) {
return ip;
}
}
return host;
}
}

View file

@ -9,7 +9,6 @@ import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
@ -23,32 +22,38 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import de.gultsch.minidns.AndroidDNSClient;
import de.measite.minidns.AbstractDNSClient;
import de.measite.minidns.DNSCache;
import de.measite.minidns.DNSClient;
import de.measite.minidns.DNSName;
import de.measite.minidns.Question;
import de.measite.minidns.Record;
import de.measite.minidns.cache.LRUCache;
import de.measite.minidns.dnssec.DNSSECResultNotAuthenticException;
import de.measite.minidns.dnsserverlookup.AndroidUsingExec;
import de.measite.minidns.hla.DnssecResolverApi;
import de.measite.minidns.hla.ResolverApi;
import de.measite.minidns.hla.ResolverResult;
import de.measite.minidns.iterative.ReliableDNSClient;
import de.measite.minidns.record.A;
import de.measite.minidns.record.AAAA;
import de.measite.minidns.record.CNAME;
import de.measite.minidns.record.Data;
import de.measite.minidns.record.InternetAddressRR;
import de.measite.minidns.record.SRV;
//import de.gultsch.minidns.AndroidDnsClient;
import org.minidns.AbstractDnsClient;
import org.minidns.DnsCache;
import org.minidns.DnsClient;
import org.minidns.DnsName.DnsName;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.record.Record;
import org.minidns.cache.LruCache;
import org.minidns.dnsserverlookup.AndroidUsingExec;
import org.minidns.hla.DnssecResolverApi;
import org.minidns.hla.ResolverApi;
import org.minidns.hla.ResolverResult;
import org.minidns.iterative.ReliableDnsClient;
import org.minidns.record.A;
import org.minidns.record.AAAA;
import org.minidns.record.CNAME;
import org.minidns.record.Data;
import org.minidns.record.InternetAddressRR;
import org.minidns.record.SRV;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import com.google.common.base.Throwables;
import com.google.common.net.InetAddresses;
import com.google.common.primitives.Ints;
import com.google.common.base.Strings;
public class Resolver {
public static final int DEFAULT_PORT_XMPP = 5222;
@ -59,49 +64,33 @@ public class Resolver {
private static XmppConnectionService SERVICE = null;
public static void init(final XmppConnectionService service) {
public static void init(XmppConnectionService service) {
Resolver.SERVICE = service;
DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
final AbstractDNSClient client = ResolverApi.INSTANCE.getClient();
if (client instanceof ReliableDNSClient) {
disableHardcodedDnsServers((ReliableDNSClient) client);
DnsClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE);
DnsClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE);
DnsClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service));
final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
if (client instanceof ReliableDnsClient) {
((ReliableDnsClient) client).setUseHardcodedDnsServers(false);
}
}
private static void disableHardcodedDnsServers(final ReliableDNSClient reliableDNSClient) {
try {
final Field dnsClientField = ReliableDNSClient.class.getDeclaredField("dnsClient");
dnsClientField.setAccessible(true);
final DNSClient dnsClient = (DNSClient) dnsClientField.get(reliableDNSClient);
if (dnsClient != null) {
dnsClient.getDataSource().setTimeout(3000);
}
final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers");
useHardcodedDnsServers.setAccessible(true);
useHardcodedDnsServers.setBoolean(dnsClient, false);
} catch (NoSuchFieldException | IllegalAccessException e) {
Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e);
}
}
public static Result fromHardCoded(final String hostname, final int port) {
final Result ipResult = fromIpAddress(hostname, port);
if (ipResult != null) {
ipResult.connect();
return ipResult;
}
return happyEyeball(resolveNoSrvRecords(DNSName.from(hostname), port, true));
public static List<Result> fromHardCoded(final String hostname, final int port) {
final Result result = new Result();
result.hostname = DnsName.from(hostname);
result.port = port;
result.directTls = useDirectTls(port);
result.authenticated = true;
return Collections.singletonList(result);
}
public static void checkDomain(final Jid jid) {
DNSName.from(jid.getDomain());
DnsName.from(jid.getDomain());
}
public static boolean invalidHostname(final String hostname) {
try {
DNSName.from(hostname);
DnsName.from(hostname);
return false;
} catch (IllegalArgumentException e) {
return true;
@ -109,11 +98,11 @@ public class Resolver {
}
public static void clearCache() {
final AbstractDNSClient client = ResolverApi.INSTANCE.getClient();
final DNSCache dnsCache = client.getCache();
if (dnsCache instanceof LRUCache) {
final AbstractDnsClient client = ResolverApi.INSTANCE.getClient();
final DnsCache dnsCache = client.getCache();
if (dnsCache instanceof LruCache) {
Log.d(Config.LOGTAG,"clearing DNS cache");
((LRUCache) dnsCache).clear();
((LruCache) dnsCache).clear();
}
}
@ -122,11 +111,10 @@ public class Resolver {
return port == 443 || port == 5223;
}
public static Result resolve(final String domain) {
final Result ipResult = fromIpAddress(domain, DEFAULT_PORT_XMPP);
if (ipResult != null) {
ipResult.connect();
return ipResult;
public static List<Result> resolve(final String domain) {
final List<Result> ipResults = (List<Result>) fromIpAddress(domain, DEFAULT_PORT_XMPP);
if (ipResults.size() > 0) {
return ipResults;
}
final List<Result> results = new ArrayList<>();
final List<Result> fallbackResults = new ArrayList<>();
@ -137,9 +125,11 @@ public class Resolver {
synchronized (results) {
results.addAll(list);
}
} catch (Throwable throwable) {
} catch (final Throwable throwable) {
if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
}
}
});
threads[1] = new Thread(() -> {
try {
@ -147,19 +137,17 @@ public class Resolver {
synchronized (results) {
results.addAll(list);
}
} catch (Throwable throwable) {
} catch (final Throwable throwable) {
if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
}
}
});
threads[2] = new Thread(() -> {
try {
final List<Result> list = resolveNoSrvRecords(DNSName.from(domain), DEFAULT_PORT_XMPP, true);
List<Result> list = resolveNoSrvRecords(DnsName.from(domain), DEFAULT_PORT_XMPP, true);
synchronized (fallbackResults) {
fallbackResults.addAll(list);
}
} catch (Throwable throwable) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": resolving no SRV record (STARTTLS)", throwable);
}
});
for (final Thread thread : threads) {
thread.start();
@ -171,44 +159,43 @@ public class Resolver {
threads[2].interrupt();
synchronized (results) {
Collections.sort(results);
return happyEyeball(results);
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString());
return new ArrayList<>(results);
}
} else {
threads[2].join();
synchronized (fallbackResults) {
Collections.sort(fallbackResults);
return happyEyeball(fallbackResults);
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString());
return new ArrayList<>(fallbackResults);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
for (Thread thread : threads) {
thread.interrupt();
}
return null;
return Collections.emptyList();
}
}
private static Result fromIpAddress(final String domain, final int port) {
private static List<Result> fromIpAddress(String domain, final int defaultPortXmpp) {
if (!IP.matches(domain)) {
return null;
return Collections.emptyList();
}
try {
final Result result = new Result();
Result result = new Result();
result.ip = InetAddress.getByName(domain);
result.hostname = DNSName.from(domain);
result.port = port;
result.authenticated = true;
return result;
return Collections.singletonList(result);
} catch (UnknownHostException e) {
e.printStackTrace();
return null;
return Collections.emptyList();
}
}
private static List<Result> resolveSrv(final String domain, final boolean directTls) throws IOException {
final DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
final ResolverResult<SRV> result = resolveWithFallback(dnsName, SRV.class);
final DnsName DnsName = DnsName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain);
final ResolverResult<SRV> result = resolveWithFallback(DnsName, SRV.class);
final List<Result> results = new ArrayList<>();
final List<Thread> threads = new ArrayList<>();
@ -286,10 +273,10 @@ public class Resolver {
return resolveIp(srv, srv.name, type, authenticated, directTls);
}
private static <D extends InternetAddressRR> List<Result> resolveIp(final SRV srv, final DNSName hostname, final Class<D> type, final boolean authenticated, final boolean directTls) {
private static <D extends InternetAddressRR> List<Result> resolveIp(final SRV srv, final DnsName hostname, final Class<D> type, final boolean authenticated, final boolean directTls) {
final List<Result> list = new ArrayList<>();
try {
ResolverResult<D> results = resolveWithFallback(hostname, type, authenticated);
ResolverResult<D> results = resolveWithFallback(hostname, type);
for (D record : results.getAnswersOrEmptySet()) {
boolean ipv4 = type == A.class;
Result resolverResult = Result.fromRecord(srv, directTls, ipv4);
@ -303,42 +290,44 @@ public class Resolver {
return list;
}
private static List<Result> resolveNoSrvRecords(final DNSName dnsName, final int port, final boolean withCnames) {
final List<Result> results = new ArrayList<>();
private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
List<Result> results = new ArrayList<>();
try {
for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) {
results.add(Result.createDefault(dnsName, aaaa.getInetAddress(), port));
ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
Log.d("WUTr", "" + aResult.isAuthenticData() + " " + aResult.getAnswersOrEmptySet());
for (A a : aResult.getAnswersOrEmptySet()) {
Result r = Result.createDefault(dnsName, a.getInetAddress());
r.authenticated = aResult.isAuthenticData();
results.add(r);
}
for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) {
results.add(Result.createDefault(dnsName, a.getInetAddress(), port));
ResolverResult<AAAA> aaaaResult = resolveWithFallback(dnsName, AAAA.class);
for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) {
Result r = Result.createDefault(dnsName, aaaa.getInetAddress());
r.authenticated = aaaaResult.isAuthenticData();
results.add(r);
}
if (results.size() == 0 && withCnames) {
for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) {
results.addAll(resolveNoSrvRecords(cname.name, port, false));
ResolverResult<CNAME> cnameResult = resolveWithFallback(dnsName, CNAME.class);
for (CNAME cname : cnameResult.getAnswersOrEmptySet()) {
for (Result r : resolveNoSrvRecords(cname.name, false)) {
r.authenticated = r.authenticated && cnameResult.isAuthenticData();
results.add(r);
}
}
} catch (Throwable throwable) {
}
} catch (final Throwable throwable) {
if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
}
results.add(Result.createDefault(dnsName, port));
}
results.add(Result.createDefault(dnsName));
return results;
}
private static <D extends Data> ResolverResult<D> resolveWithFallback(final DNSName dnsName, final Class<D> type) throws IOException {
return resolveWithFallback(dnsName, type, validateHostname());
}
private static <D extends Data> ResolverResult<D> resolveWithFallback(final DNSName dnsName, final Class<D> type, final boolean validateHostname) throws IOException {
private static <D extends Data> ResolverResult<D> resolveWithFallback(DnsName dnsName, Class<D> type) throws IOException {
final Question question = new Question(dnsName, Record.TYPE.getType(type));
if (!validateHostname) {
final AndroidDNSClient androidDNSClient = new AndroidDNSClient(SERVICE);
final ResolverApi resolverApi = new ResolverApi(androidDNSClient);
return resolverApi.resolve(question);
}
try {
return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question);
} catch (DNSSECResultNotAuthenticException e) {
Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e);
return DnssecResolverApi.INSTANCE.resolve(question);
} catch (IOException e) {
throw e;
} catch (Throwable throwable) {
@ -395,10 +384,6 @@ public class Resolver {
}
}
private static boolean validateHostname() {
return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname);
}
public static class Result implements Comparable<Result>, Callable<Result> {
public static final String DOMAIN = "domain";
public static final String IP = "ip";
@ -410,7 +395,7 @@ public class Resolver {
public static final String TIME_REQUESTED = "time_requested";
private InetAddress ip;
private DNSName hostname;
private DnsName hostname;
private int port = DEFAULT_PORT_XMPP;
private boolean directTls = false;
private boolean authenticated = false;
@ -437,7 +422,7 @@ public class Resolver {
return result;
}
static Result createDefault(final DNSName hostname, final int port) {
static Result createDefault(final DnsName hostname, final int port) {
InetAddress ip = null;
try {
ip = InetAddress.getByName(hostname.toString());
@ -447,7 +432,7 @@ public class Resolver {
return createDefault(hostname, ip, port);
}
static Result createDefault(final DNSName hostname, final InetAddress ip, final int port) {
static Result createDefault(final DnsName hostname, final InetAddress ip, final int port) {
Result result = new Result();
result.timeRequested = System.currentTimeMillis();
result.port = port;
@ -483,7 +468,7 @@ public class Resolver {
return result;
}
public DNSName getHostname() {
public DnsName getHostname() {
return hostname;
}
@ -587,7 +572,7 @@ public class Resolver {
result.ip = null;
}
final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME));
result.hostname = hostname == null ? null : DNSName.from(hostname);
result.hostname = hostname == null ? null : DnsName.from(hostname);
result.port = cursor.getInt(cursor.getColumnIndex(PORT));
result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0;
result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0;
@ -607,5 +592,64 @@ public class Resolver {
contentValues.put(TIME_REQUESTED, timeRequested);
return contentValues;
}
public Result seeOtherHost(final String seeOtherHost) {
final String hostname = seeOtherHost.trim();
if (hostname.isEmpty()) {
return null;
}
final Result result = new Result();
result.directTls = this.directTls;
final int portSegmentStart = hostname.lastIndexOf(':');
if (hostname.charAt(hostname.length() - 1) != ']'
&& portSegmentStart >= 0
&& hostname.length() >= portSegmentStart + 1) {
final String hostPart = hostname.substring(0, portSegmentStart);
final String portPart = hostname.substring(portSegmentStart + 1);
final Integer port = Ints.tryParse(portPart);
if (port == null || Strings.isNullOrEmpty(hostPart)) {
return null;
}
final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
result.port = port;
if (InetAddresses.isInetAddress(host)) {
final InetAddress inetAddress;
try {
inetAddress = InetAddresses.forString(host);
} catch (final IllegalArgumentException e) {
return null;
}
result.ip = inetAddress;
} else {
if (hostPart.trim().isEmpty()) {
return null;
}
try {
result.hostname = DnsName.from(hostPart.trim());
} catch (final Exception e) {
return null;
}
}
} else {
final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
if (InetAddresses.isInetAddress(host)) {
final InetAddress inetAddress;
try {
inetAddress = InetAddresses.forString(host);
} catch (final IllegalArgumentException e) {
return null;
}
result.ip = inetAddress;
} else {
try {
result.hostname = DnsName.from(hostname);
} catch (final Exception e) {
return null;
}
}
result.port = port;
}
return result;
}
}
}

View file

@ -206,6 +206,8 @@ public class XmppConnection implements Runnable {
private HashedToken.Mechanism hashTokenRequest;
private HttpUrl redirectionUrl = null;
private String verifiedHostname = null;
private Resolver.Result currentResolverResult;
private Resolver.Result seeOtherHostResolverResult;
private volatile Thread mThread;
private CountDownLatch mStreamCountDownLatch;
private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
@ -243,6 +245,12 @@ public class XmppConnection implements Runnable {
}
}
public boolean resolverAuthenticated() {
if (currentResolverResult == null) return false;
return currentResolverResult.isAuthenticated();
}
private void changeStatus(final Account.State nextStatus) {
synchronized (this) {
if (Thread.currentThread().isInterrupted()) {
@ -409,7 +417,7 @@ public class XmppConnection implements Runnable {
}
} else {
final String domain = account.getServer();
Resolver.Result results;
final List<Resolver.Result> results;
final boolean hardcoded = extended && !account.getHostname().isEmpty();
if (hardcoded) {
results = Resolver.fromHardCoded(account.getHostname(), account.getPort());
@ -433,7 +441,7 @@ public class XmppConnection implements Runnable {
storedBackupResult =
mXmppConnectionService.databaseBackend.findResolverResult(domain);
if (storedBackupResult != null && results != storedBackupResult && !storedBackupResult.isOutdated()) {
results = storedBackupResult;
results = (List<Resolver.Result>) storedBackupResult;
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@ -441,6 +449,15 @@ public class XmppConnection implements Runnable {
+ storedBackupResult);
}
}
final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
if (seeOtherHost != null) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0");
results.add(0, seeOtherHost);
}
for (final Iterator<Resolver.Result> iterator = results.iterator();
iterator.hasNext(); ) {
final Resolver.Result result = iterator.next();
if (results == null || results.getSocket() == null) {
results = Resolver.resolve(domain);
}
@ -457,7 +474,6 @@ public class XmppConnection implements Runnable {
// if tls is true, encryption is implied and must not be started
features.encryptionEnabled = results.isDirectTls();
verifiedHostname = results.isAuthenticated() ? results.getHostname().toString() : null;
Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname);
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString()
+ ": using values from resolver " + results.toString());
@ -476,6 +492,8 @@ public class XmppConnection implements Runnable {
mXmppConnectionService.databaseBackend.saveResolverResult(
domain, results);
}
this.currentResolverResult = result;
this.seeOtherHostResolverResult = null;
// successfully connected to server that speaks xmpp
} else {
FileBackend.close(localSocket);
@ -501,6 +519,7 @@ public class XmppConnection implements Runnable {
throw new UnknownHostException();
}
}
}
processStream();
} catch (final SecurityException e) {
this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
@ -2331,6 +2350,21 @@ public class XmppConnection implements Runnable {
failPendingMessages(text);
}
throw new StateChangingException(Account.State.POLICY_VIOLATION);
} else if (streamError.hasChild("see-other-host")) {
final String seeOtherHost = streamError.findChildContent("see-other-host");
final Resolver.Result currentResolverResult = this.currentResolverResult;
if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
throw new StateChangingException(Account.State.STREAM_ERROR);
}
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult);
final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
if (seeOtherResult != null) {
this.seeOtherHostResolverResult = seeOtherResult;
throw new StateChangingException(Account.State.SEE_OTHER_HOST);
} else {
throw new StateChangingException(Account.State.STREAM_ERROR);
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
throw new StateChangingException(Account.State.STREAM_ERROR);

View file

@ -572,13 +572,13 @@ public class JingleRtpConnection extends AbstractJingleConnection
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
return;
}
processCandidates(receivedContentAccept.contents.entrySet());
updateEndUserState();
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": remote has accepted content-add "
+ ContentAddition.summary(receivedContentAccept));
processCandidates(receivedContentAccept.contents.entrySet());
updateEndUserState();
}
private void receiveContentModify(final JinglePacket jinglePacket) {

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480ZM480,680Q497,680 509.5,667.5Q522,655 522,638Q522,621 509.5,608.5Q497,596 480,596Q463,596 450.5,608.5Q438,621 438,638Q438,655 450.5,667.5Q463,680 480,680ZM451,552L511,552Q511,541 511,530Q511,519 516,509Q522,495 532,485.5Q542,476 553,466Q570,449 582.5,428Q595,407 595,382Q595,337 560.5,308.5Q526,280 480,280Q440,280 408.5,303Q377,326 366,364L420,386Q426,366 442.5,352Q459,338 480,338Q502,338 518.5,351Q535,364 535,384Q535,401 524.5,415.5Q514,430 501,442Q489,453 477,464.5Q465,476 458,491Q451,505 451,520.5Q451,536 451,552Z"/>
</vector>

View file

@ -58,12 +58,29 @@
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="horizontal">
<ImageView
android:id="@+id/verification_indicator"
android:layout_width="?attr/TextSizeCaption"
android:layout_height="?attr/TextSizeCaption"
android:layout_gravity="center_vertical"
android:layout_marginRight="4sp"
android:alpha="0.70"
android:gravity="center_vertical"
android:src="@drawable/shield_question" />
<TextView
android:id="@+id/account_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/account_status_unknown"
android:textAppearance="@style/TextAppearance.Conversations.Body2" />
</LinearLayout>>
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch

View file

@ -269,6 +269,42 @@ android:fontFamily="notosansregular"
android:visibility="gone" />
</LinearLayout>
<RelativeLayout
android:id="@+id/verification_box"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="12dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_toLeftOf="@+id/verification_indicator"
android:orientation="vertical">
<TextView
android:id="@+id/verification_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Not DNSSEC Verified"
android:textAppearance="@style/TextAppearance.Conversations.Body1.Tertiary" />
</LinearLayout>
<ImageView
android:id="@+id/verification_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:alpha="?attr/icon_alpha"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/image_button_padding"
android:src="@drawable/shield_question"
android:visibility="visible" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/your_name_box"
android:layout_width="wrap_content"

View file

@ -561,6 +561,7 @@
<string name="pref_export_plain_text_logs_summary">Enable an export of chat logs as human readable text files</string>
<string name="pref_export_plain_text_logs">Export human readable chat logs</string>
<string name="payment_required">Payment required</string>
<string name="reconnect_on_other_host">Reconnect on other host</string>
<string name="missing_internet_permission">Missing internet permission</string>
<string name="me">Me</string>
<string name="contact_asks_for_presence_subscription">Contact asks for presence subscription</string>

View file

@ -666,11 +666,13 @@
android:key="dont_trust_system_cas"
android:summary="@string/pref_dont_trust_system_cas_summary"
android:title="@string/pref_dont_trust_system_cas_title" />
<!-- TODO: Actually not needed anymore since it is automatically validated now
<SwitchPreference
android:defaultValue="@bool/validate_hostname"
android:key="validate_hostname"
android:summary="@string/pref_validate_hostname_summary"
android:title="@string/pref_validate_hostname" />
-->
<Preference
android:key="remove_trusted_certificates"
android:summary="@string/pref_remove_trusted_certificates_summary"