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}