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