QuickConversationsService.java

  1package eu.siacs.conversations.services;
  2
  3
  4import android.content.SharedPreferences;
  5import android.os.SystemClock;
  6import android.preference.PreferenceManager;
  7import android.util.Log;
  8
  9import java.io.BufferedWriter;
 10import java.io.OutputStream;
 11import java.io.OutputStreamWriter;
 12import java.net.ConnectException;
 13import java.net.HttpURLConnection;
 14import java.net.URL;
 15import java.net.UnknownHostException;
 16import java.security.SecureRandom;
 17import java.util.ArrayList;
 18import java.util.Collections;
 19import java.util.List;
 20import java.util.Locale;
 21import java.util.Map;
 22import java.util.Set;
 23import java.util.UUID;
 24import java.util.WeakHashMap;
 25import java.util.concurrent.atomic.AtomicBoolean;
 26
 27import javax.net.ssl.SSLHandshakeException;
 28
 29import eu.siacs.conversations.Config;
 30import eu.siacs.conversations.android.PhoneNumberContact;
 31import eu.siacs.conversations.crypto.sasl.Plain;
 32import eu.siacs.conversations.entities.Account;
 33import eu.siacs.conversations.utils.AccountUtils;
 34import eu.siacs.conversations.utils.CryptoHelper;
 35import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 36import eu.siacs.conversations.xml.Element;
 37import eu.siacs.conversations.xml.Namespace;
 38import eu.siacs.conversations.xmpp.XmppConnection;
 39import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 40import io.michaelrocks.libphonenumber.android.Phonenumber;
 41import rocks.xmpp.addr.Jid;
 42
 43public class QuickConversationsService extends AbstractQuickConversationsService {
 44
 45
 46    public static final int API_ERROR_OTHER = -1;
 47    public static final int API_ERROR_UNKNOWN_HOST = -2;
 48    public static final int API_ERROR_CONNECT = -3;
 49    public static final int API_ERROR_SSL_HANDSHAKE = -4;
 50    public static final int API_ERROR_AIRPLANE_MODE = -5;
 51
 52    private static final String BASE_URL = "http://venus.fritz.box:4567";
 53
 54    private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
 55
 56    private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
 57    private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 58
 59    private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
 60    private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
 61
 62    QuickConversationsService(XmppConnectionService xmppConnectionService) {
 63        super(xmppConnectionService);
 64    }
 65
 66    public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 67        synchronized (mOnVerificationRequested) {
 68            mOnVerificationRequested.add(onVerificationRequested);
 69        }
 70    }
 71
 72    public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 73        synchronized (mOnVerificationRequested) {
 74            mOnVerificationRequested.remove(onVerificationRequested);
 75        }
 76    }
 77
 78    public void addOnVerificationListener(OnVerification onVerification) {
 79        synchronized (mOnVerification) {
 80            mOnVerification.add(onVerification);
 81        }
 82    }
 83
 84    public void removeOnVerificationListener(OnVerification onVerification) {
 85        synchronized (mOnVerification) {
 86            mOnVerification.remove(onVerification);
 87        }
 88    }
 89
 90    public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
 91        final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
 92        if (mVerificationRequestInProgress.compareAndSet(false, true)) {
 93            new Thread(() -> {
 94                try {
 95
 96                    Thread.sleep(5000);
 97
 98                    final URL url = new URL(BASE_URL + "/authentication/" + e164);
 99                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
100                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
101                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
102                    setHeader(connection);
103                    final int code = connection.getResponseCode();
104                    if (code == 200) {
105                        createAccountAndWait(phoneNumber, 0L);
106                    } else if (code == 429) {
107                        createAccountAndWait(phoneNumber, retryAfter(connection));
108                    } else {
109                        synchronized (mOnVerificationRequested) {
110                            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
111                                onVerificationRequested.onVerificationRequestFailed(code);
112                            }
113                        }
114                    }
115                } catch (Exception e) {
116                    final int code = getApiErrorCode(e);
117                    synchronized (mOnVerificationRequested) {
118                        for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
119                            onVerificationRequested.onVerificationRequestFailed(code);
120                        }
121                    }
122                } finally {
123                    mVerificationRequestInProgress.set(false);
124                }
125            }).start();
126        }
127
128
129    }
130
131    private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
132        String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
133        Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
134        Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, null);
135        Account account = AccountUtils.getFirst(service);
136        if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
137            if (account != null) {
138                service.deleteAccount(account);
139            }
140            account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
141            account.setOption(Account.OPTION_DISABLED, true);
142            account.setOption(Account.OPTION_UNVERIFIED, true);
143            service.createAccount(account);
144        }
145        synchronized (mOnVerificationRequested) {
146            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
147                if (timestamp <= 0) {
148                    onVerificationRequested.onVerificationRequested();
149                } else {
150                    onVerificationRequested.onVerificationRequestedRetryAt(timestamp);
151                }
152            }
153        }
154    }
155
156    public void verify(final Account account, String pin) {
157        if (mVerificationInProgress.compareAndSet(false, true)) {
158            new Thread(() -> {
159                try {
160
161                    Thread.sleep(5000);
162
163                    final URL url = new URL(BASE_URL + "/password");
164                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
165                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
166                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
167                    connection.setRequestMethod("POST");
168                    connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
169                    setHeader(connection);
170                    final OutputStream os = connection.getOutputStream();
171                    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
172                    writer.write(account.getPassword());
173                    writer.flush();
174                    writer.close();
175                    os.close();
176                    connection.connect();
177                    final int code = connection.getResponseCode();
178                    if (code == 200) {
179                        account.setOption(Account.OPTION_UNVERIFIED, false);
180                        account.setOption(Account.OPTION_DISABLED, false);
181                        service.updateAccount(account);
182                        synchronized (mOnVerification) {
183                            for (OnVerification onVerification : mOnVerification) {
184                                onVerification.onVerificationSucceeded();
185                            }
186                        }
187                    } else if (code == 429) {
188                        final long retryAfter = retryAfter(connection);
189                        synchronized (mOnVerification) {
190                            for (OnVerification onVerification : mOnVerification) {
191                                onVerification.onVerificationRetryAt(retryAfter);
192                            }
193                        }
194                    } else {
195                        synchronized (mOnVerification) {
196                            for (OnVerification onVerification : mOnVerification) {
197                                onVerification.onVerificationFailed(code);
198                            }
199                        }
200                    }
201                } catch (Exception e) {
202                    final int code = getApiErrorCode(e);
203                    synchronized (mOnVerification) {
204                        for (OnVerification onVerification : mOnVerification) {
205                            onVerification.onVerificationFailed(code);
206                        }
207                    }
208                } finally {
209                    mVerificationInProgress.set(false);
210                }
211            }).start();
212        }
213    }
214
215    private void setHeader(HttpURLConnection connection) {
216        connection.setRequestProperty("User-Agent", service.getIqGenerator().getUserAgent());
217        connection.setRequestProperty("Installation-Id", getInstallationId());
218        connection.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
219    }
220
221    private String getInstallationId() {
222        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
223        String id = preferences.getString(INSTALLATION_ID, null);
224        if (id != null) {
225            return id;
226        } else {
227            id = UUID.randomUUID().toString();
228            preferences.edit().putString(INSTALLATION_ID, id).apply();
229            return id;
230        }
231
232    }
233
234    private int getApiErrorCode(Exception e) {
235        if (!service.hasInternetConnection()) {
236            return API_ERROR_AIRPLANE_MODE;
237        } else if (e instanceof UnknownHostException) {
238            return API_ERROR_UNKNOWN_HOST;
239        } else if (e instanceof ConnectException) {
240            return API_ERROR_CONNECT;
241        } else if (e instanceof SSLHandshakeException) {
242            return API_ERROR_SSL_HANDSHAKE;
243        } else {
244            Log.d(Config.LOGTAG, e.getClass().getName());
245            return API_ERROR_OTHER;
246        }
247    }
248
249    private static long retryAfter(HttpURLConnection connection) {
250        try {
251            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
252        } catch (Exception e) {
253            return 0;
254        }
255    }
256
257    public boolean isVerifying() {
258        return mVerificationInProgress.get();
259    }
260
261    public boolean isRequestingVerification() {
262        return mVerificationRequestInProgress.get();
263    }
264
265    @Override
266    public void considerSync() {
267        Map<String, PhoneNumberContact> contacts = PhoneNumberContact.load(service);
268        for(Account account : service.getAccounts()) {
269            considerSync(account, contacts);
270        }
271    }
272
273    private void considerSync(Account account, Map<String, PhoneNumberContact> contacts) {
274        XmppConnection xmppConnection = account.getXmppConnection();
275        Jid syncServer = xmppConnection == null ? null : xmppConnection.findDiscoItemByFeature(Namespace.SYNCHRONIZATION);
276        if (syncServer == null) {
277            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": skipping sync. no sync server found");
278            return;
279        }
280        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sending phone list to "+syncServer);
281        List<Element> entries = new ArrayList<>();
282        for(PhoneNumberContact c : contacts.values()) {
283            entries.add(new Element("entry").setAttribute("number",c.getPhoneNumber()));
284        }
285        Element phoneBook = new Element("phone-book",Namespace.SYNCHRONIZATION);
286        phoneBook.setChildren(entries);
287        IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
288        iqPacket.setTo(syncServer);
289        iqPacket.addChild(phoneBook);
290        service.sendIqPacket(account, iqPacket, null);
291    }
292
293    public interface OnVerificationRequested {
294        void onVerificationRequestFailed(int code);
295
296        void onVerificationRequested();
297
298        void onVerificationRequestedRetryAt(long timestamp);
299    }
300
301    public interface OnVerification {
302        void onVerificationFailed(int code);
303
304        void onVerificationSucceeded();
305
306        void onVerificationRetryAt(long timestamp);
307    }
308}