Skip to content
Snippets Groups Projects
Unverified Commit 16cc3234 authored by Daniel Gultsch's avatar Daniel Gultsch
Browse files

use common trust manager in quicksy

parent 8ac6d793
No related branches found
No related tags found
No related merge requests found
package eu.siacs.conversations.services;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import com.google.common.collect.ImmutableMap;
import de.gultsch.common.TrustManagers;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.android.PhoneNumberContact;
import eu.siacs.conversations.crypto.TrustManagers;
import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
......@@ -30,11 +26,8 @@ import eu.siacs.conversations.utils.TLSSocketFactory;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.model.stanza.Iq;
import io.michaelrocks.libphonenumber.android.Phonenumber;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
......@@ -63,7 +56,6 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
......@@ -73,7 +65,6 @@ import javax.net.ssl.X509TrustManager;
public class QuickConversationsService extends AbstractQuickConversationsService {
public static final int API_ERROR_OTHER = -1;
public static final int API_ERROR_UNKNOWN_HOST = -2;
public static final int API_ERROR_CONNECT = -3;
......@@ -87,8 +78,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private static final String BASE_URL = "https://" + API_DOMAIN;
private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
private final Set<OnVerificationRequested> mOnVerificationRequested =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<OnVerification> mOnVerification =
Collections.newSetFromMap(new WeakHashMap<>());
private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
......@@ -97,7 +90,8 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private Attempt mLastSyncAttempt = Attempt.NULL;
private final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
private final SerialSingleThreadExecutor mSerialSingleThreadExecutor =
new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
QuickConversationsService(XmppConnectionService xmppConnectionService) {
super(xmppConnectionService);
......@@ -105,19 +99,22 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private static long retryAfter(HttpURLConnection connection) {
try {
return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
return SystemClock.elapsedRealtime()
+ (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
} catch (Exception e) {
return 0;
}
}
public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
public void addOnVerificationRequestedListener(
OnVerificationRequested onVerificationRequested) {
synchronized (mOnVerificationRequested) {
mOnVerificationRequested.add(onVerificationRequested);
}
}
public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
public void removeOnVerificationRequestedListener(
OnVerificationRequested onVerificationRequested) {
synchronized (mOnVerificationRequested) {
mOnVerificationRequested.remove(onVerificationRequested);
}
......@@ -139,62 +136,63 @@ public class QuickConversationsService extends AbstractQuickConversationsService
final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
if (mVerificationRequestInProgress.compareAndSet(false, true)) {
SmsRetrieverWrapper.start(service);
new Thread(() -> {
try {
final URL url = new URL(BASE_URL + "/authentication/" + e164);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
setBundledLetsEncrypt(service, connection);
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
setHeader(connection);
final int code = connection.getResponseCode();
if (code == 200) {
createAccountAndWait(phoneNumber, 0L);
} else if (code == 429) {
createAccountAndWait(phoneNumber, retryAfter(connection));
} else {
synchronized (mOnVerificationRequested) {
for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
onVerificationRequested.onVerificationRequestFailed(code);
}
}
}
} catch (IOException e) {
final int code = getApiErrorCode(e);
synchronized (mOnVerificationRequested) {
for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
onVerificationRequested.onVerificationRequestFailed(code);
}
}
} finally {
mVerificationRequestInProgress.set(false);
}
}).start();
new Thread(
() -> {
try {
final URL url = new URL(BASE_URL + "/authentication/" + e164);
final HttpURLConnection connection =
(HttpURLConnection) url.openConnection();
setBundledLetsEncrypt(service, connection);
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
setHeader(connection);
final int code = connection.getResponseCode();
if (code == 200) {
createAccountAndWait(phoneNumber, 0L);
} else if (code == 429) {
createAccountAndWait(phoneNumber, retryAfter(connection));
} else {
synchronized (mOnVerificationRequested) {
for (OnVerificationRequested onVerificationRequested :
mOnVerificationRequested) {
onVerificationRequested.onVerificationRequestFailed(
code);
}
}
}
} catch (IOException e) {
final int code = getApiErrorCode(e);
synchronized (mOnVerificationRequested) {
for (OnVerificationRequested onVerificationRequested :
mOnVerificationRequested) {
onVerificationRequested.onVerificationRequestFailed(
code);
}
}
} finally {
mVerificationRequestInProgress.set(false);
}
})
.start();
}
}
private static void setBundledLetsEncrypt(
final Context context, final HttpURLConnection connection) {
if (connection instanceof HttpsURLConnection httpsURLConnection) {
final X509TrustManager trustManager;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
try {
trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context);
} catch (final NoSuchAlgorithmException
| KeyStoreException
| CertificateException
| IOException e) {
Log.e(Config.LOGTAG, "could not configured bundled LetsEncrypt", e);
return;
}
} else {
return;
}
final SSLSocketFactory socketFactory;
try {
socketFactory =
new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM);
} catch (final KeyManagementException | NoSuchAlgorithmException e) {
new TLSSocketFactory(
new X509TrustManager[] {
TrustManagers.createForAndroidVersion(context)
},
SECURE_RANDOM);
} catch (final KeyManagementException
| NoSuchAlgorithmException
| KeyStoreException
| CertificateException
| IOException e) {
Log.e(Config.LOGTAG, "could not configured bundled LetsEncrypt", e);
return;
}
......@@ -211,7 +209,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
Log.d(
Config.LOGTAG,
"requesting verification for "
+ PhoneNumberUtilWrapper.normalize(service, phoneNumber));
Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, null);
Account account = AccountUtils.getFirst(service);
if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
......@@ -237,64 +238,74 @@ public class QuickConversationsService extends AbstractQuickConversationsService
public void verify(final Account account, String pin) {
if (mVerificationInProgress.compareAndSet(false, true)) {
new Thread(() -> {
try {
final URL url = new URL(BASE_URL + "/password");
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
setBundledLetsEncrypt(service, connection);
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
setHeader(connection);
final OutputStream os = connection.getOutputStream();
final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
writer.write(account.getPassword());
writer.flush();
writer.close();
os.close();
connection.connect();
final int code = connection.getResponseCode();
if (code == 200 || code == 201) {
account.setOption(Account.OPTION_UNVERIFIED, false);
account.setOption(Account.OPTION_DISABLED, false);
awaitingAccountStateChange = new CountDownLatch(1);
service.updateAccount(account);
try {
awaitingAccountStateChange.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": timer expired while waiting for account to connect");
}
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationSucceeded();
}
}
} else if (code == 429) {
final long retryAfter = retryAfter(connection);
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationRetryAt(retryAfter);
}
}
} else {
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationFailed(code);
}
}
}
} catch (IOException e) {
final int code = getApiErrorCode(e);
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationFailed(code);
}
}
} finally {
mVerificationInProgress.set(false);
}
}).start();
new Thread(
() -> {
try {
final URL url = new URL(BASE_URL + "/password");
final HttpURLConnection connection =
(HttpURLConnection) url.openConnection();
setBundledLetsEncrypt(service, connection);
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setRequestMethod("POST");
connection.setRequestProperty(
"Authorization",
Plain.getMessage(account.getUsername(), pin));
setHeader(connection);
final OutputStream os = connection.getOutputStream();
final BufferedWriter writer =
new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
writer.write(account.getPassword());
writer.flush();
writer.close();
os.close();
connection.connect();
final int code = connection.getResponseCode();
if (code == 200 || code == 201) {
account.setOption(Account.OPTION_UNVERIFIED, false);
account.setOption(Account.OPTION_DISABLED, false);
awaitingAccountStateChange = new CountDownLatch(1);
service.updateAccount(account);
try {
awaitingAccountStateChange.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": timer expired while waiting for"
+ " account to connect");
}
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationSucceeded();
}
}
} else if (code == 429) {
final long retryAfter = retryAfter(connection);
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationRetryAt(retryAfter);
}
}
} else {
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationFailed(code);
}
}
}
} catch (IOException e) {
final int code = getApiErrorCode(e);
synchronized (mOnVerification) {
for (OnVerification onVerification : mOnVerification) {
onVerification.onVerificationFailed(code);
}
}
} finally {
mVerificationInProgress.set(false);
}
})
.start();
}
}
......@@ -339,7 +350,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService
return mVerificationRequestInProgress.get();
}
@Override
public boolean isSynchronizing() {
return mRunningSyncJobs.get() > 0;
......@@ -353,12 +363,13 @@ public class QuickConversationsService extends AbstractQuickConversationsService
@Override
public void considerSyncBackground(final boolean forced) {
mRunningSyncJobs.incrementAndGet();
mSerialSingleThreadExecutor.execute(() -> {
considerSync(forced);
if (mRunningSyncJobs.decrementAndGet() == 0) {
service.updateRosterUi();
}
});
mSerialSingleThreadExecutor.execute(
() -> {
considerSync(forced);
if (mRunningSyncJobs.decrementAndGet() == 0) {
service.updateRosterUi();
}
});
}
@Override
......@@ -380,16 +391,19 @@ public class QuickConversationsService extends AbstractQuickConversationsService
onVerification.startBackgroundVerification(pin);
}
}
}
private void considerSync(boolean forced) {
final ImmutableMap<String, PhoneNumberContact> allContacts = PhoneNumberContact.load(service);
final ImmutableMap<String, PhoneNumberContact> allContacts =
PhoneNumberContact.load(service);
for (final Account account : service.getAccounts()) {
final Map<String, PhoneNumberContact> contacts = filtered(allContacts, account.getJid().getLocal());
final Map<String, PhoneNumberContact> contacts =
filtered(allContacts, account.getJid().getLocal());
if (contacts.size() < allContacts.size()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": found own phone number in address book. ignoring...");
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": found own phone number in address book. ignoring...");
}
refresh(account, contacts.values());
if (!considerSync(account, contacts, forced)) {
......@@ -408,17 +422,24 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
private void refresh(Account account, Collection<PhoneNumberContact> contacts) {
for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
for (Contact contact :
account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
final Uri uri = contact.getSystemAccount();
if (uri == null) {
continue;
}
final String number = getNumber(contact);
final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(contacts, uri, number);
final PhoneNumberContact phoneNumberContact =
PhoneNumberContact.findByUriOrNumber(contacts, uri, number);
final boolean needsCacheClean;
if (phoneNumberContact != null) {
if (!uri.equals(phoneNumberContact.getLookupUri())) {
Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri());
Log.d(
Config.LOGTAG,
"lookupUri has changed from "
+ uri
+ " to "
+ phoneNumberContact.getLookupUri());
}
needsCacheClean = contact.setPhoneContact(phoneNumberContact);
} else {
......@@ -439,7 +460,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
return null;
}
private boolean considerSync(final Account account, final Map<String, PhoneNumberContact> contacts, final boolean forced) {
private boolean considerSync(
final Account account,
final Map<String, PhoneNumberContact> contacts,
final boolean forced) {
final int hash = contacts.keySet().hashCode();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
if (!mLastSyncAttempt.retry(hash) && !forced) {
......@@ -448,59 +472,79 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
mRunningSyncJobs.incrementAndGet();
final Jid syncServer = Jid.of(API_DOMAIN);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending phone list to " + syncServer);
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": sending phone list to " + syncServer);
final List<Element> entries = new ArrayList<>();
for (final PhoneNumberContact c : contacts.values()) {
entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber()));
}
final Iq query = new Iq(Iq.Type.GET);
query.setTo(syncServer);
final Element book = new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries);
final String statusQuo = Entry.statusQuo(contacts.values(), account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
final Element book =
new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries);
final String statusQuo =
Entry.statusQuo(
contacts.values(),
account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
book.setAttribute("ver", statusQuo);
query.addChild(book);
mLastSyncAttempt = Attempt.create(hash);
service.sendIqPacket(account, query, (response) -> {
if (response.getType() == Iq.Type.RESULT) {
final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION);
if (phoneBook != null) {
final List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
for (Entry entry : Entry.ofPhoneBook(phoneBook)) {
final PhoneNumberContact phoneContact = contacts.get(entry.getNumber());
if (phoneContact == null) {
continue;
}
for (final Jid jid : entry.getJids()) {
final Contact contact = account.getRoster().getContact(jid);
final boolean needsCacheClean = contact.setPhoneContact(phoneContact);
if (needsCacheClean) {
service.getAvatarService().clear(contact);
service.sendIqPacket(
account,
query,
(response) -> {
if (response.getType() == Iq.Type.RESULT) {
final Element phoneBook =
response.findChild("phone-book", Namespace.SYNCHRONIZATION);
if (phoneBook != null) {
final List<Contact> withSystemAccounts =
account.getRoster()
.getWithSystemAccounts(PhoneNumberContact.class);
for (Entry entry : Entry.ofPhoneBook(phoneBook)) {
final PhoneNumberContact phoneContact =
contacts.get(entry.getNumber());
if (phoneContact == null) {
continue;
}
for (final Jid jid : entry.getJids()) {
final Contact contact = account.getRoster().getContact(jid);
final boolean needsCacheClean =
contact.setPhoneContact(phoneContact);
if (needsCacheClean) {
service.getAvatarService().clear(contact);
}
withSystemAccounts.remove(contact);
}
}
withSystemAccounts.remove(contact);
}
}
for (final Contact contact : withSystemAccounts) {
final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
if (needsCacheClean) {
service.getAvatarService().clear(contact);
for (final Contact contact : withSystemAccounts) {
final boolean needsCacheClean =
contact.unsetPhoneContact(PhoneNumberContact.class);
if (needsCacheClean) {
service.getAvatarService().clear(contact);
}
}
} else {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": phone number contact list remains unchanged");
}
} else if (response.getType() == Iq.Type.TIMEOUT) {
mLastSyncAttempt = Attempt.NULL;
} else {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": failed to sync contact list with api server");
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": phone number contact list remains unchanged");
}
} else if (response.getType() == Iq.Type.TIMEOUT) {
mLastSyncAttempt = Attempt.NULL;
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to sync contact list with api server");
}
mRunningSyncJobs.decrementAndGet();
service.syncRoster(account);
service.updateRosterUi();
});
mRunningSyncJobs.decrementAndGet();
service.syncRoster(account);
service.updateRosterUi();
});
return true;
}
public interface OnVerificationRequested {
void onVerificationRequestFailed(int code);
......@@ -535,7 +579,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
public boolean retry(int hash) {
return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL;
return hash != this.hash
|| SystemClock.elapsedRealtime() - timestamp
>= Config.CONTACT_SYNC_RETRY_INTERVAL;
}
}
}
\ No newline at end of file
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment