QuickConversationsService.java

  1package eu.siacs.conversations.services;
  2
  3
  4import android.os.SystemClock;
  5import android.util.Log;
  6
  7import java.io.BufferedWriter;
  8import java.io.OutputStream;
  9import java.io.OutputStreamWriter;
 10import java.net.ConnectException;
 11import java.net.HttpURLConnection;
 12import java.net.URL;
 13import java.net.URLConnection;
 14import java.net.UnknownHostException;
 15import java.security.SecureRandom;
 16import java.util.Collections;
 17import java.util.Set;
 18import java.util.WeakHashMap;
 19import java.util.concurrent.atomic.AtomicBoolean;
 20
 21import eu.siacs.conversations.Config;
 22import eu.siacs.conversations.crypto.sasl.Plain;
 23import eu.siacs.conversations.entities.Account;
 24import eu.siacs.conversations.utils.AccountUtils;
 25import eu.siacs.conversations.utils.CryptoHelper;
 26import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 27import io.michaelrocks.libphonenumber.android.Phonenumber;
 28import rocks.xmpp.addr.Jid;
 29
 30public class QuickConversationsService {
 31
 32
 33    public static final int API_ERROR_OTHER = -1;
 34    public static final int API_ERROR_UNKNOWN_HOST = -2;
 35    public static final int API_ERROR_CONNECT = -3;
 36
 37    private static final String BASE_URL = "https://venus.fritz.box:4567";
 38
 39    private final XmppConnectionService service;
 40
 41    private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
 42    private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 43
 44    private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
 45    private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
 46
 47    QuickConversationsService(XmppConnectionService xmppConnectionService) {
 48        this.service = xmppConnectionService;
 49    }
 50
 51    public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 52        synchronized (mOnVerificationRequested) {
 53            mOnVerificationRequested.add(onVerificationRequested);
 54        }
 55    }
 56
 57    public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 58        synchronized (mOnVerificationRequested) {
 59            mOnVerificationRequested.remove(onVerificationRequested);
 60        }
 61    }
 62
 63    public void addOnVerificationListener(OnVerification onVerification) {
 64        synchronized (mOnVerification) {
 65            mOnVerification.add(onVerification);
 66        }
 67    }
 68
 69    public void removeOnVerificationListener(OnVerification onVerification) {
 70        synchronized (mOnVerification) {
 71            mOnVerification.remove(onVerification);
 72        }
 73    }
 74
 75    public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
 76        final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
 77
 78        /**
 79         * GET /authentication/+phoneNumber
 80         *
 81         * - returns too many requests, (sms ist unterwegs), retry after seconden -- auf jeden fall in nächste activity (verify activity) weiter leiten weil es sein kann das sms noch ankommt
 82         * - returns OK; success (auch in activity weiter lassen. aber ohne error paramater; dh send again button is activ; und vielleicht kurzer toast bzw snackbar
 83         * - returns invalid request user error wenn die phone number falsch ist
 84         */
 85        if (mVerificationRequestInProgress.compareAndSet(false, true)) {
 86            new Thread(() -> {
 87                try {
 88
 89                    Thread.sleep(5000);
 90
 91                    final URL url = new URL(BASE_URL + "/authentication/" + e164);
 92                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 93                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
 94                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
 95                    final int code = connection.getResponseCode();
 96                    if (code == 200) {
 97                        createAccountAndWait(phoneNumber, 0L);
 98                    } else if (code == 429) {
 99                        createAccountAndWait(phoneNumber, retryAfter(connection));
100                    } else {
101                        synchronized (mOnVerificationRequested) {
102                            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
103                                onVerificationRequested.onVerificationRequestFailed(code);
104                            }
105                        }
106                    }
107                } catch (Exception e) {
108                    final int code = getApiErrorCode(e);
109                    synchronized (mOnVerificationRequested) {
110                        for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
111                            onVerificationRequested.onVerificationRequestFailed(code);
112                        }
113                    }
114                } finally {
115                    mVerificationRequestInProgress.set(false);
116                }
117            }).start();
118        }
119
120
121    }
122
123    private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
124        String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
125        Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
126        Jid jid = Jid.of(local, "quick.conversations.im", null);
127        Account account = AccountUtils.getFirst(service);
128        if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
129            if (account != null) {
130                service.deleteAccount(account);
131            }
132            account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
133            account.setOption(Account.OPTION_DISABLED, true);
134            account.setOption(Account.OPTION_UNVERIFIED, true);
135            service.createAccount(account);
136        }
137        synchronized (mOnVerificationRequested) {
138            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
139                if (timestamp <= 0) {
140                    onVerificationRequested.onVerificationRequested();
141                } else {
142                    onVerificationRequested.onVerificationRequestedRetryAt(timestamp);
143                }
144            }
145        }
146    }
147
148    public void verify(Account account, String pin) {
149        /**
150         * POST /password
151         * authentication gesetzt mit telephone nummber und verification code
152         * body = password
153         *
154         * retry after, too many requests
155         * code wrong
156         * OK (weiterleiten auf publish avatar activity
157         *
158         */
159        if (mVerificationInProgress.compareAndSet(false, true)) {
160            new Thread(() -> {
161                try {
162
163                    Thread.sleep(5000);
164
165                    final URL url = new URL(BASE_URL + "/password");
166                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
167                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
168                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
169                    connection.setRequestMethod("POST");
170                    connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
171                    final OutputStream os = connection.getOutputStream();
172                    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
173                    writer.write(account.getPassword());
174                    writer.flush();
175                    writer.close();
176                    os.close();
177                    connection.connect();
178                    final int code = connection.getResponseCode();
179                    if (code == 200) {
180                        synchronized (mOnVerification) {
181                            for (OnVerification onVerification : mOnVerification) {
182                                onVerification.onVerificationSucceeded();
183                            }
184                        }
185                    } else if (code == 429) {
186                        final long retryAfter = retryAfter(connection);
187                        synchronized (mOnVerification) {
188                            for (OnVerification onVerification : mOnVerification) {
189                                onVerification.onVerificationRetryAt(retryAfter);
190                            }
191                        }
192                    } else {
193                        synchronized (mOnVerification) {
194                            for (OnVerification onVerification : mOnVerification) {
195                                onVerification.onVerificationFailed(code);
196                            }
197                        }
198                    }
199                } catch (Exception e) {
200                    final int code = getApiErrorCode(e);
201                    synchronized (mOnVerification) {
202                        for (OnVerification onVerification : mOnVerification) {
203                            onVerification.onVerificationFailed(code);
204                        }
205                    }
206                } finally {
207                    mVerificationInProgress.set(false);
208                }
209            }).start();
210        }
211    }
212
213    private static int getApiErrorCode(Exception e) {
214        if (e instanceof UnknownHostException) {
215            return API_ERROR_UNKNOWN_HOST;
216        } else if (e instanceof ConnectException) {
217            return API_ERROR_CONNECT;
218        } else {
219            Log.d(Config.LOGTAG,e.getClass().getName());
220            return API_ERROR_OTHER;
221        }
222    }
223
224    private static long retryAfter(HttpURLConnection connection) {
225        try {
226            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
227        } catch (Exception e) {
228            return 0;
229        }
230    }
231
232    public boolean isVerifying() {
233        return mVerificationInProgress.get();
234    }
235
236    public boolean isRequestingVerification() {
237        return mVerificationRequestInProgress.get();
238    }
239
240    public interface OnVerificationRequested {
241        void onVerificationRequestFailed(int code);
242
243        void onVerificationRequested();
244
245        void onVerificationRequestedRetryAt(long timestamp);
246    }
247
248    public interface OnVerification {
249        void onVerificationFailed(int code);
250
251        void onVerificationSucceeded();
252
253        void onVerificationRetryAt(long timestamp);
254    }
255}