WebViewLoginController.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. /*
  2. *
  3. * Nextcloud Talk application
  4. *
  5. * @author Mario Danic
  6. * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. package com.nextcloud.talk.controllers;
  22. import android.annotation.SuppressLint;
  23. import android.content.pm.ActivityInfo;
  24. import android.net.http.SslCertificate;
  25. import android.net.http.SslError;
  26. import android.os.Build;
  27. import android.os.Bundle;
  28. import android.security.KeyChain;
  29. import android.security.KeyChainException;
  30. import android.text.TextUtils;
  31. import android.view.LayoutInflater;
  32. import android.view.View;
  33. import android.view.ViewGroup;
  34. import android.webkit.*;
  35. import android.widget.ProgressBar;
  36. import androidx.annotation.NonNull;
  37. import androidx.work.OneTimeWorkRequest;
  38. import androidx.work.WorkManager;
  39. import autodagger.AutoInjector;
  40. import butterknife.BindView;
  41. import com.bluelinelabs.conductor.RouterTransaction;
  42. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
  43. import com.bluelinelabs.logansquare.LoganSquare;
  44. import com.nextcloud.talk.R;
  45. import com.nextcloud.talk.application.NextcloudTalkApplication;
  46. import com.nextcloud.talk.controllers.base.BaseController;
  47. import com.nextcloud.talk.events.CertificateEvent;
  48. import com.nextcloud.talk.jobs.PushRegistrationWorker;
  49. import com.nextcloud.talk.models.LoginData;
  50. import com.nextcloud.talk.models.database.UserEntity;
  51. import com.nextcloud.talk.models.json.push.PushConfigurationState;
  52. import com.nextcloud.talk.utils.bundle.BundleKeys;
  53. import com.nextcloud.talk.utils.database.user.UserUtils;
  54. import com.nextcloud.talk.utils.preferences.AppPreferences;
  55. import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder;
  56. import com.nextcloud.talk.utils.ssl.MagicTrustManager;
  57. import io.reactivex.android.schedulers.AndroidSchedulers;
  58. import io.reactivex.disposables.Disposable;
  59. import io.reactivex.schedulers.Schedulers;
  60. import io.requery.Persistable;
  61. import io.requery.reactivex.ReactiveEntityStore;
  62. import org.greenrobot.eventbus.EventBus;
  63. import javax.inject.Inject;
  64. import java.io.IOException;
  65. import java.lang.reflect.Field;
  66. import java.net.CookieManager;
  67. import java.net.URLDecoder;
  68. import java.security.PrivateKey;
  69. import java.security.cert.CertificateException;
  70. import java.security.cert.X509Certificate;
  71. import java.util.HashMap;
  72. import java.util.Locale;
  73. import java.util.Map;
  74. @AutoInjector(NextcloudTalkApplication.class)
  75. public class WebViewLoginController extends BaseController {
  76. public static final String TAG = "WebViewLoginController";
  77. private final String PROTOCOL_SUFFIX = "://";
  78. private final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";
  79. @Inject
  80. UserUtils userUtils;
  81. @Inject
  82. AppPreferences appPreferences;
  83. @Inject
  84. ReactiveEntityStore<Persistable> dataStore;
  85. @Inject
  86. MagicTrustManager magicTrustManager;
  87. @Inject
  88. EventBus eventBus;
  89. @Inject
  90. CookieManager cookieManager;
  91. @BindView(R.id.webview)
  92. WebView webView;
  93. @BindView(R.id.progress_bar)
  94. ProgressBar progressBar;
  95. private String assembledPrefix;
  96. private Disposable userQueryDisposable;
  97. private String baseUrl;
  98. private boolean isPasswordUpdate;
  99. private String username;
  100. private String password;
  101. private int loginStep = 0;
  102. private boolean automatedLoginAttempted = false;
  103. public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) {
  104. this.baseUrl = baseUrl;
  105. this.isPasswordUpdate = isPasswordUpdate;
  106. }
  107. public WebViewLoginController(String baseUrl, boolean isPasswordUpdate, String username, String password) {
  108. this.baseUrl = baseUrl;
  109. this.isPasswordUpdate = isPasswordUpdate;
  110. this.username = username;
  111. this.password = password;
  112. }
  113. public WebViewLoginController(Bundle args) {
  114. super(args);
  115. }
  116. private String getWebLoginUserAgent() {
  117. return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
  118. Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " ("
  119. + getResources().getString(R.string.nc_app_name) + ")";
  120. }
  121. @Override
  122. protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
  123. return inflater.inflate(R.layout.controller_web_view_login, container, false);
  124. }
  125. @SuppressLint("SetJavaScriptEnabled")
  126. @Override
  127. protected void onViewBound(@NonNull View view) {
  128. super.onViewBound(view);
  129. NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
  130. if (getActivity() != null) {
  131. getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
  132. }
  133. if (getActionBar() != null) {
  134. getActionBar().hide();
  135. }
  136. assembledPrefix = getResources().getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/";
  137. webView.getSettings().setAllowFileAccess(false);
  138. webView.getSettings().setAllowFileAccessFromFileURLs(false);
  139. webView.getSettings().setJavaScriptEnabled(true);
  140. webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false);
  141. webView.getSettings().setDomStorageEnabled(true);
  142. webView.getSettings().setUserAgentString(getWebLoginUserAgent());
  143. webView.getSettings().setSaveFormData(false);
  144. webView.getSettings().setSavePassword(false);
  145. webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
  146. webView.clearCache(true);
  147. webView.clearFormData();
  148. webView.clearHistory();
  149. WebView.clearClientCertPreferences(null);
  150. CookieSyncManager.createInstance(getActivity());
  151. android.webkit.CookieManager.getInstance().removeAllCookies(null);
  152. Map<String, String> headers = new HashMap<>();
  153. headers.put("OCS-APIRequest", "true");
  154. webView.setWebViewClient(new WebViewClient() {
  155. private boolean basePageLoaded;
  156. @Override
  157. public boolean shouldOverrideUrlLoading(WebView view, String url) {
  158. if (url.startsWith(assembledPrefix)) {
  159. parseAndLoginFromWebView(url);
  160. return true;
  161. }
  162. return false;
  163. }
  164. @Override
  165. public void onPageFinished(WebView view, String url) {
  166. loginStep++;
  167. if (!basePageLoaded) {
  168. if (progressBar != null) {
  169. progressBar.setVisibility(View.GONE);
  170. }
  171. if (webView != null) {
  172. webView.setVisibility(View.VISIBLE);
  173. }
  174. basePageLoaded = true;
  175. }
  176. if (!TextUtils.isEmpty(username) && webView != null) {
  177. if (loginStep == 1) {
  178. webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };");
  179. } else if (!automatedLoginAttempted) {
  180. automatedLoginAttempted = true;
  181. if (TextUtils.isEmpty(password)) {
  182. webView.loadUrl("javascript:var justStore = document.getElementById('user').value = '" + username + "';");
  183. } else {
  184. webView.loadUrl("javascript: {" +
  185. "document.getElementById('user').value = '" + username + "';" +
  186. "document.getElementById('password').value = '" + password + "';" +
  187. "document.getElementById('submit').click(); };");
  188. }
  189. }
  190. }
  191. super.onPageFinished(view, url);
  192. }
  193. @Override
  194. public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
  195. UserEntity userEntity = userUtils.getCurrentUser();
  196. String alias = null;
  197. if (!isPasswordUpdate) {
  198. alias = appPreferences.getTemporaryClientCertAlias();
  199. }
  200. if (TextUtils.isEmpty(alias) && (userEntity != null)) {
  201. alias = userEntity.getClientCertificate();
  202. }
  203. if (!TextUtils.isEmpty(alias)) {
  204. String finalAlias = alias;
  205. new Thread(() -> {
  206. try {
  207. PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias);
  208. X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), finalAlias);
  209. if (privateKey != null && certificates != null) {
  210. request.proceed(privateKey, certificates);
  211. } else {
  212. request.cancel();
  213. }
  214. } catch (KeyChainException | InterruptedException e) {
  215. request.cancel();
  216. }
  217. }).start();
  218. } else {
  219. KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> {
  220. if (chosenAlias != null) {
  221. appPreferences.setTemporaryClientCertAlias(chosenAlias);
  222. new Thread(() -> {
  223. PrivateKey privateKey = null;
  224. try {
  225. privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias);
  226. X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), chosenAlias);
  227. if (privateKey != null && certificates != null) {
  228. request.proceed(privateKey, certificates);
  229. } else {
  230. request.cancel();
  231. }
  232. } catch (KeyChainException | InterruptedException e) {
  233. request.cancel();
  234. }
  235. }).start();
  236. } else {
  237. request.cancel();
  238. }
  239. }, new String[]{"RSA", "EC"}, null, request.getHost(), request.getPort(), null);
  240. }
  241. }
  242. @Override
  243. public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
  244. try {
  245. SslCertificate sslCertificate = error.getCertificate();
  246. Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate");
  247. f.setAccessible(true);
  248. X509Certificate cert = (X509Certificate) f.get(sslCertificate);
  249. if (cert == null) {
  250. handler.cancel();
  251. } else {
  252. try {
  253. magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic");
  254. handler.proceed();
  255. } catch (CertificateException exception) {
  256. eventBus.post(new CertificateEvent(cert, magicTrustManager, handler));
  257. }
  258. }
  259. } catch (Exception exception) {
  260. handler.cancel();
  261. }
  262. }
  263. @Override
  264. public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
  265. super.onReceivedError(view, errorCode, description, failingUrl);
  266. }
  267. });
  268. webView.loadUrl(baseUrl + "/index.php/login/flow", headers);
  269. }
  270. private void dispose() {
  271. if (userQueryDisposable != null && !userQueryDisposable.isDisposed()) {
  272. userQueryDisposable.dispose();
  273. }
  274. userQueryDisposable = null;
  275. }
  276. private void parseAndLoginFromWebView(String dataString) {
  277. LoginData loginData = parseLoginData(assembledPrefix, dataString);
  278. if (loginData != null) {
  279. dispose();
  280. UserEntity currentUser = userUtils.getCurrentUser();
  281. ApplicationWideMessageHolder.MessageType messageType = null;
  282. if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(), baseUrl)) {
  283. messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED;
  284. }
  285. if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) {
  286. ApplicationWideMessageHolder.getInstance().setMessageType(
  287. ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION);
  288. if (!isPasswordUpdate) {
  289. getRouter().popToRoot();
  290. } else {
  291. getRouter().popCurrentController();
  292. }
  293. }
  294. ApplicationWideMessageHolder.MessageType finalMessageType = messageType;
  295. cookieManager.getCookieStore().removeAll();
  296. if (!isPasswordUpdate && finalMessageType == null) {
  297. Bundle bundle = new Bundle();
  298. bundle.putString(BundleKeys.KEY_USERNAME, loginData.getUsername());
  299. bundle.putString(BundleKeys.KEY_TOKEN, loginData.getToken());
  300. bundle.putString(BundleKeys.KEY_BASE_URL, loginData.getServerUrl());
  301. String protocol = "";
  302. if (baseUrl.startsWith("http://")) {
  303. protocol = "http://";
  304. } else if (baseUrl.startsWith("https://")) {
  305. protocol = "https://";
  306. }
  307. if (!TextUtils.isEmpty(protocol)) {
  308. bundle.putString(BundleKeys.KEY_ORIGINAL_PROTOCOL, protocol);
  309. }
  310. getRouter().pushController(RouterTransaction.with(new AccountVerificationController
  311. (bundle)).pushChangeHandler(new HorizontalChangeHandler())
  312. .popChangeHandler(new HorizontalChangeHandler()));
  313. } else {
  314. if (isPasswordUpdate) {
  315. if (currentUser != null) {
  316. userQueryDisposable = userUtils.createOrUpdateUser(null, loginData.getToken(),
  317. null, null, "", true,
  318. null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null)
  319. .subscribeOn(Schedulers.newThread())
  320. .observeOn(AndroidSchedulers.mainThread())
  321. .subscribe(userEntity -> {
  322. if (finalMessageType != null) {
  323. ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
  324. }
  325. OneTimeWorkRequest pushRegistrationWork = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class).build();
  326. WorkManager.getInstance().enqueue(pushRegistrationWork);
  327. getRouter().popCurrentController();
  328. }, throwable -> dispose(),
  329. this::dispose);
  330. }
  331. } else {
  332. if (finalMessageType != null) {
  333. ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
  334. }
  335. getRouter().popToRoot();
  336. }
  337. }
  338. }
  339. }
  340. private LoginData parseLoginData(String prefix, String dataString) {
  341. if (dataString.length() < prefix.length()) {
  342. return null;
  343. }
  344. LoginData loginData = new LoginData();
  345. // format is xxx://login/server:xxx&user:xxx&password:xxx
  346. String data = dataString.substring(prefix.length());
  347. String[] values = data.split("&");
  348. if (values.length != 3) {
  349. return null;
  350. }
  351. for (String value : values) {
  352. if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
  353. loginData.setUsername(URLDecoder.decode(
  354. value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
  355. } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
  356. loginData.setToken(URLDecoder.decode(
  357. value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
  358. } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
  359. loginData.setServerUrl(URLDecoder.decode(
  360. value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
  361. } else {
  362. return null;
  363. }
  364. }
  365. if (!TextUtils.isEmpty(loginData.getServerUrl()) && !TextUtils.isEmpty(loginData.getUsername()) &&
  366. !TextUtils.isEmpty(loginData.getToken())) {
  367. return loginData;
  368. } else {
  369. return null;
  370. }
  371. }
  372. @Override
  373. public void onDestroy() {
  374. super.onDestroy();
  375. dispose();
  376. }
  377. @Override
  378. protected void onDestroyView(@NonNull View view) {
  379. super.onDestroyView(view);
  380. if (getActivity() != null) {
  381. getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
  382. }
  383. }
  384. }