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