WebViewLoginController.java 18 KB

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