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