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