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