WebViewLoginController.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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.graphics.Bitmap;
  25. import android.net.http.SslCertificate;
  26. import android.net.http.SslError;
  27. import android.os.Build;
  28. import android.os.Bundle;
  29. import android.security.KeyChain;
  30. import android.security.KeyChainException;
  31. import android.text.TextUtils;
  32. import android.view.LayoutInflater;
  33. import android.view.View;
  34. import android.view.ViewGroup;
  35. import android.webkit.ClientCertRequest;
  36. import android.webkit.CookieSyncManager;
  37. import android.webkit.SslErrorHandler;
  38. import android.webkit.WebResourceRequest;
  39. import android.webkit.WebResourceResponse;
  40. import android.webkit.WebSettings;
  41. import android.webkit.WebView;
  42. import android.webkit.WebViewClient;
  43. import android.widget.ProgressBar;
  44. import com.bluelinelabs.conductor.RouterTransaction;
  45. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
  46. import com.nextcloud.talk.R;
  47. import com.nextcloud.talk.application.NextcloudTalkApplication;
  48. import com.nextcloud.talk.controllers.base.BaseController;
  49. import com.nextcloud.talk.events.CertificateEvent;
  50. import com.nextcloud.talk.jobs.PushRegistrationWorker;
  51. import com.nextcloud.talk.models.LoginData;
  52. import com.nextcloud.talk.models.database.UserEntity;
  53. import com.nextcloud.talk.utils.DisplayUtils;
  54. import com.nextcloud.talk.utils.bundle.BundleKeys;
  55. import com.nextcloud.talk.utils.database.user.UserUtils;
  56. import com.nextcloud.talk.utils.preferences.AppPreferences;
  57. import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder;
  58. import com.nextcloud.talk.utils.ssl.MagicTrustManager;
  59. import org.greenrobot.eventbus.EventBus;
  60. import java.lang.reflect.Field;
  61. import java.net.CookieManager;
  62. import java.net.URLDecoder;
  63. import java.security.PrivateKey;
  64. import java.security.cert.CertificateException;
  65. import java.security.cert.X509Certificate;
  66. import java.util.HashMap;
  67. import java.util.Locale;
  68. import java.util.Map;
  69. import javax.inject.Inject;
  70. import androidx.annotation.NonNull;
  71. import androidx.appcompat.app.AppCompatActivity;
  72. import androidx.core.content.res.ResourcesCompat;
  73. import androidx.work.OneTimeWorkRequest;
  74. import androidx.work.WorkManager;
  75. import autodagger.AutoInjector;
  76. import butterknife.BindView;
  77. import de.cotech.hw.fido.WebViewFidoBridge;
  78. import io.reactivex.android.schedulers.AndroidSchedulers;
  79. import io.reactivex.disposables.Disposable;
  80. import io.reactivex.schedulers.Schedulers;
  81. import io.requery.Persistable;
  82. import io.requery.reactivex.ReactiveEntityStore;
  83. @AutoInjector(NextcloudTalkApplication.class)
  84. public class WebViewLoginController extends BaseController {
  85. public static final String TAG = "WebViewLoginController";
  86. private final String PROTOCOL_SUFFIX = "://";
  87. private final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";
  88. @Inject
  89. UserUtils userUtils;
  90. @Inject
  91. AppPreferences appPreferences;
  92. @Inject
  93. ReactiveEntityStore<Persistable> dataStore;
  94. @Inject
  95. MagicTrustManager magicTrustManager;
  96. @Inject
  97. EventBus eventBus;
  98. @Inject
  99. CookieManager cookieManager;
  100. @BindView(R.id.webview)
  101. WebView webView;
  102. @BindView(R.id.progress_bar)
  103. ProgressBar progressBar;
  104. private String assembledPrefix;
  105. private Disposable userQueryDisposable;
  106. private String baseUrl;
  107. private boolean isPasswordUpdate;
  108. private String username;
  109. private String password;
  110. private int loginStep = 0;
  111. private boolean automatedLoginAttempted = false;
  112. private WebViewFidoBridge webViewFidoBridge;
  113. public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) {
  114. this.baseUrl = baseUrl;
  115. this.isPasswordUpdate = isPasswordUpdate;
  116. }
  117. public WebViewLoginController(String baseUrl, boolean isPasswordUpdate, String username, String password) {
  118. this.baseUrl = baseUrl;
  119. this.isPasswordUpdate = isPasswordUpdate;
  120. this.username = username;
  121. this.password = password;
  122. }
  123. public WebViewLoginController(Bundle args) {
  124. super(args);
  125. }
  126. private String getWebLoginUserAgent() {
  127. return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
  128. Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " ("
  129. + getResources().getString(R.string.nc_app_name) + ")";
  130. }
  131. @Override
  132. protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
  133. return inflater.inflate(R.layout.controller_web_view_login, container, false);
  134. }
  135. @SuppressLint("SetJavaScriptEnabled")
  136. @Override
  137. protected void onViewBound(@NonNull View view) {
  138. super.onViewBound(view);
  139. NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
  140. if (getActivity() != null) {
  141. getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
  142. }
  143. if (getActionBar() != null) {
  144. getActionBar().hide();
  145. }
  146. assembledPrefix = getResources().getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/";
  147. webView.getSettings().setAllowFileAccess(false);
  148. webView.getSettings().setAllowFileAccessFromFileURLs(false);
  149. webView.getSettings().setJavaScriptEnabled(true);
  150. webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false);
  151. webView.getSettings().setDomStorageEnabled(true);
  152. webView.getSettings().setUserAgentString(getWebLoginUserAgent());
  153. webView.getSettings().setSaveFormData(false);
  154. webView.getSettings().setSavePassword(false);
  155. webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
  156. webView.clearCache(true);
  157. webView.clearFormData();
  158. webView.clearHistory();
  159. WebView.clearClientCertPreferences(null);
  160. webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView((AppCompatActivity) getActivity(), webView);
  161. CookieSyncManager.createInstance(getActivity());
  162. android.webkit.CookieManager.getInstance().removeAllCookies(null);
  163. Map<String, String> headers = new HashMap<>();
  164. headers.put("OCS-APIRequest", "true");
  165. webView.setWebViewClient(new WebViewClient() {
  166. private boolean basePageLoaded;
  167. @Override
  168. public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
  169. webViewFidoBridge.delegateShouldInterceptRequest(view, request);
  170. return super.shouldInterceptRequest(view, request);
  171. }
  172. @Override
  173. public void onPageStarted(WebView view, String url, Bitmap favicon) {
  174. super.onPageStarted(view, url, favicon);
  175. webViewFidoBridge.delegateOnPageStarted(view, url, favicon);
  176. }
  177. @Override
  178. public boolean shouldOverrideUrlLoading(WebView view, String url) {
  179. if (url.startsWith(assembledPrefix)) {
  180. parseAndLoginFromWebView(url);
  181. return true;
  182. }
  183. return false;
  184. }
  185. @Override
  186. public void onPageFinished(WebView view, String url) {
  187. loginStep++;
  188. if (!basePageLoaded) {
  189. if (progressBar != null) {
  190. progressBar.setVisibility(View.GONE);
  191. }
  192. if (webView != null) {
  193. webView.setVisibility(View.VISIBLE);
  194. }
  195. basePageLoaded = true;
  196. }
  197. if (!TextUtils.isEmpty(username) && webView != null) {
  198. if (loginStep == 1) {
  199. webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };");
  200. } else if (!automatedLoginAttempted) {
  201. automatedLoginAttempted = true;
  202. if (TextUtils.isEmpty(password)) {
  203. webView.loadUrl("javascript:var justStore = document.getElementById('user').value = '" + username + "';");
  204. } else {
  205. webView.loadUrl("javascript: {" +
  206. "document.getElementById('user').value = '" + username + "';" +
  207. "document.getElementById('password').value = '" + password + "';" +
  208. "document.getElementById('submit').click(); };");
  209. }
  210. }
  211. }
  212. super.onPageFinished(view, url);
  213. }
  214. @Override
  215. public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
  216. UserEntity userEntity = userUtils.getCurrentUser();
  217. String alias = null;
  218. if (!isPasswordUpdate) {
  219. alias = appPreferences.getTemporaryClientCertAlias();
  220. }
  221. if (TextUtils.isEmpty(alias) && (userEntity != null)) {
  222. alias = userEntity.getClientCertificate();
  223. }
  224. if (!TextUtils.isEmpty(alias)) {
  225. String finalAlias = alias;
  226. new Thread(() -> {
  227. try {
  228. PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias);
  229. X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), finalAlias);
  230. if (privateKey != null && certificates != null) {
  231. request.proceed(privateKey, certificates);
  232. } else {
  233. request.cancel();
  234. }
  235. } catch (KeyChainException | InterruptedException e) {
  236. request.cancel();
  237. }
  238. }).start();
  239. } else {
  240. KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> {
  241. if (chosenAlias != null) {
  242. appPreferences.setTemporaryClientCertAlias(chosenAlias);
  243. new Thread(() -> {
  244. PrivateKey privateKey = null;
  245. try {
  246. privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias);
  247. X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), chosenAlias);
  248. if (privateKey != null && certificates != null) {
  249. request.proceed(privateKey, certificates);
  250. } else {
  251. request.cancel();
  252. }
  253. } catch (KeyChainException | InterruptedException e) {
  254. request.cancel();
  255. }
  256. }).start();
  257. } else {
  258. request.cancel();
  259. }
  260. }, new String[]{"RSA", "EC"}, null, request.getHost(), request.getPort(), null);
  261. }
  262. }
  263. @Override
  264. public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
  265. try {
  266. SslCertificate sslCertificate = error.getCertificate();
  267. Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate");
  268. f.setAccessible(true);
  269. X509Certificate cert = (X509Certificate) f.get(sslCertificate);
  270. if (cert == null) {
  271. handler.cancel();
  272. } else {
  273. try {
  274. magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic");
  275. handler.proceed();
  276. } catch (CertificateException exception) {
  277. eventBus.post(new CertificateEvent(cert, magicTrustManager, handler));
  278. }
  279. }
  280. } catch (Exception exception) {
  281. handler.cancel();
  282. }
  283. }
  284. @Override
  285. public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
  286. super.onReceivedError(view, errorCode, description, failingUrl);
  287. }
  288. });
  289. webView.loadUrl(baseUrl + "/index.php/login/flow", headers);
  290. }
  291. private void dispose() {
  292. if (userQueryDisposable != null && !userQueryDisposable.isDisposed()) {
  293. userQueryDisposable.dispose();
  294. }
  295. userQueryDisposable = null;
  296. }
  297. private void parseAndLoginFromWebView(String dataString) {
  298. LoginData loginData = parseLoginData(assembledPrefix, dataString);
  299. if (loginData != null) {
  300. dispose();
  301. UserEntity currentUser = userUtils.getCurrentUser();
  302. ApplicationWideMessageHolder.MessageType messageType = null;
  303. if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(), baseUrl)) {
  304. messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED;
  305. }
  306. if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) {
  307. ApplicationWideMessageHolder.getInstance().setMessageType(
  308. ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION);
  309. if (!isPasswordUpdate) {
  310. getRouter().popToRoot();
  311. } else {
  312. getRouter().popCurrentController();
  313. }
  314. }
  315. ApplicationWideMessageHolder.MessageType finalMessageType = messageType;
  316. cookieManager.getCookieStore().removeAll();
  317. if (!isPasswordUpdate && finalMessageType == null) {
  318. Bundle bundle = new Bundle();
  319. bundle.putString(BundleKeys.INSTANCE.getKEY_USERNAME(), loginData.getUsername());
  320. bundle.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), loginData.getToken());
  321. bundle.putString(BundleKeys.INSTANCE.getKEY_BASE_URL(), loginData.getServerUrl());
  322. String protocol = "";
  323. if (baseUrl.startsWith("http://")) {
  324. protocol = "http://";
  325. } else if (baseUrl.startsWith("https://")) {
  326. protocol = "https://";
  327. }
  328. if (!TextUtils.isEmpty(protocol)) {
  329. bundle.putString(BundleKeys.INSTANCE.getKEY_ORIGINAL_PROTOCOL(), protocol);
  330. }
  331. getRouter().pushController(RouterTransaction.with(new AccountVerificationController
  332. (bundle)).pushChangeHandler(new HorizontalChangeHandler())
  333. .popChangeHandler(new HorizontalChangeHandler()));
  334. } else {
  335. if (isPasswordUpdate) {
  336. if (currentUser != null) {
  337. userQueryDisposable = userUtils.createOrUpdateUser(null, loginData.getToken(),
  338. null, null, "", true,
  339. null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null)
  340. .subscribeOn(Schedulers.io())
  341. .observeOn(AndroidSchedulers.mainThread())
  342. .subscribe(userEntity -> {
  343. if (finalMessageType != null) {
  344. ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
  345. }
  346. OneTimeWorkRequest pushRegistrationWork = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class).build();
  347. WorkManager.getInstance().enqueue(pushRegistrationWork);
  348. getRouter().popCurrentController();
  349. }, throwable -> dispose(),
  350. this::dispose);
  351. }
  352. } else {
  353. if (finalMessageType != null) {
  354. // FIXME when the user registers a new account that was setup before (aka
  355. // ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED)
  356. // The token is not updated in the database and therefor the account not visible/usable
  357. ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
  358. }
  359. getRouter().popToRoot();
  360. }
  361. }
  362. }
  363. }
  364. private LoginData parseLoginData(String prefix, String dataString) {
  365. if (dataString.length() < prefix.length()) {
  366. return null;
  367. }
  368. LoginData loginData = new LoginData();
  369. // format is xxx://login/server:xxx&user:xxx&password:xxx
  370. String data = dataString.substring(prefix.length());
  371. String[] values = data.split("&");
  372. if (values.length != 3) {
  373. return null;
  374. }
  375. for (String value : values) {
  376. if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
  377. loginData.setUsername(URLDecoder.decode(
  378. value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
  379. } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
  380. loginData.setToken(URLDecoder.decode(
  381. value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
  382. } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
  383. loginData.setServerUrl(URLDecoder.decode(
  384. value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
  385. } else {
  386. return null;
  387. }
  388. }
  389. if (!TextUtils.isEmpty(loginData.getServerUrl()) && !TextUtils.isEmpty(loginData.getUsername()) &&
  390. !TextUtils.isEmpty(loginData.getToken())) {
  391. return loginData;
  392. } else {
  393. return null;
  394. }
  395. }
  396. @Override
  397. protected void onAttach(@NonNull View view) {
  398. super.onAttach(view);
  399. if (getActivity() != null && getResources() != null) {
  400. DisplayUtils.applyColorToStatusBar(getActivity(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));
  401. DisplayUtils.applyColorToNavigationBar(getActivity().getWindow(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));
  402. }
  403. }
  404. @Override
  405. public void onDestroy() {
  406. super.onDestroy();
  407. dispose();
  408. }
  409. @Override
  410. protected void onDestroyView(@NonNull View view) {
  411. super.onDestroyView(view);
  412. if (getActivity() != null) {
  413. getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
  414. }
  415. }
  416. @Override
  417. public AppBarLayoutType getAppBarLayoutType() {
  418. return AppBarLayoutType.EMPTY;
  419. }
  420. }