/* * * Nextcloud Talk application * * @author Mario Danic * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.nextcloud.talk.controllers; import android.annotation.SuppressLint; import android.content.pm.ActivityInfo; import android.net.http.SslCertificate; import android.net.http.SslError; import android.os.Build; import android.os.Bundle; import android.security.KeyChain; import android.security.KeyChainException; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.ClientCertRequest; import android.webkit.CookieSyncManager; import android.webkit.SslErrorHandler; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; import com.bluelinelabs.conductor.RouterTransaction; import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler; import com.nextcloud.talk.R; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.controllers.base.BaseController; import com.nextcloud.talk.events.CertificateEvent; import com.nextcloud.talk.models.LoginData; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.utils.bundle.BundleKeys; import com.nextcloud.talk.utils.database.user.UserUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder; import com.nextcloud.talk.utils.ssl.MagicTrustManager; import org.greenrobot.eventbus.EventBus; import java.lang.reflect.Field; import java.net.CookieManager; import java.net.URLDecoder; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.inject.Inject; import androidx.annotation.NonNull; import autodagger.AutoInjector; import butterknife.BindView; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.requery.Persistable; import io.requery.reactivex.ReactiveEntityStore; @AutoInjector(NextcloudTalkApplication.class) public class WebViewLoginController extends BaseController { public static final String TAG = "WebViewLoginController"; private final String PROTOCOL_SUFFIX = "://"; private final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; @Inject UserUtils userUtils; @Inject AppPreferences appPreferences; @Inject ReactiveEntityStore dataStore; @Inject MagicTrustManager magicTrustManager; @Inject EventBus eventBus; @Inject CookieManager cookieManager; @BindView(R.id.webview) WebView webView; @BindView(R.id.progress_bar) ProgressBar progressBar; private String assembledPrefix; private Disposable userQueryDisposable; private String baseUrl; private boolean isPasswordUpdate; private String username; private String password; private int loginStep = 0; private boolean automatedLoginAttempted = false; public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) { this.baseUrl = baseUrl; this.isPasswordUpdate = isPasswordUpdate; } public WebViewLoginController(String baseUrl, boolean isPasswordUpdate, String username, String password) { this.baseUrl = baseUrl; this.isPasswordUpdate = isPasswordUpdate; this.username = username; this.password = password; } public WebViewLoginController(Bundle args) { super(args); } private String getWebLoginUserAgent() { return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " (" + getResources().getString(R.string.nc_app_name) + ")"; } @Override protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { return inflater.inflate(R.layout.controller_web_view_login, container, false); } @SuppressLint("SetJavaScriptEnabled") @Override protected void onViewBound(@NonNull View view) { super.onViewBound(view); NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this); if (getActivity() != null) { getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } if (getActionBar() != null) { getActionBar().hide(); } assembledPrefix = getResources().getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"; webView.getSettings().setAllowFileAccess(false); webView.getSettings().setAllowFileAccessFromFileURLs(false); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false); webView.getSettings().setDomStorageEnabled(true); webView.getSettings().setUserAgentString(getWebLoginUserAgent()); webView.getSettings().setSaveFormData(false); webView.getSettings().setSavePassword(false); webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH); webView.clearCache(true); webView.clearFormData(); webView.clearHistory(); WebView.clearClientCertPreferences(null); CookieSyncManager.createInstance(getActivity()); android.webkit.CookieManager.getInstance().removeAllCookies(null); Map headers = new HashMap<>(); headers.put("OCS-APIRequest", "true"); webView.setWebViewClient(new WebViewClient() { private boolean basePageLoaded; @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith(assembledPrefix)) { parseAndLoginFromWebView(url); return true; } return false; } @Override public void onPageFinished(WebView view, String url) { loginStep++; if (!basePageLoaded) { if (progressBar != null) { progressBar.setVisibility(View.GONE); } if (webView != null) { webView.setVisibility(View.VISIBLE); } basePageLoaded = true; } if (!TextUtils.isEmpty(username) && webView != null) { if (loginStep == 1) { webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };"); } else if (!automatedLoginAttempted) { automatedLoginAttempted = true; if (TextUtils.isEmpty(password)) { webView.loadUrl("javascript:var justStore = document.getElementById('user').value = '" + username + "';"); } else { webView.loadUrl("javascript: {" + "document.getElementById('user').value = '" + username + "';" + "document.getElementById('password').value = '" + password + "';" + "document.getElementById('submit').click(); };"); } } } super.onPageFinished(view, url); } @Override public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { UserEntity userEntity = userUtils.getCurrentUser(); String alias = null; if (!isPasswordUpdate) { alias = appPreferences.getTemporaryClientCertAlias(); } if (TextUtils.isEmpty(alias) && (userEntity != null)) { alias = userEntity.getClientCertificate(); } if (!TextUtils.isEmpty(alias)) { String finalAlias = alias; new Thread(() -> { try { PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias); X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), finalAlias); if (privateKey != null && certificates != null) { request.proceed(privateKey, certificates); } else { request.cancel(); } } catch (KeyChainException | InterruptedException e) { request.cancel(); } }).start(); } else { KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> { if (chosenAlias != null) { appPreferences.setTemporaryClientCertAlias(chosenAlias); new Thread(() -> { PrivateKey privateKey = null; try { privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias); X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), chosenAlias); if (privateKey != null && certificates != null) { request.proceed(privateKey, certificates); } else { request.cancel(); } } catch (KeyChainException | InterruptedException e) { request.cancel(); } }).start(); } else { request.cancel(); } }, new String[]{"RSA", "EC"}, null, request.getHost(), request.getPort(), null); } } @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { try { SslCertificate sslCertificate = error.getCertificate(); Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate"); f.setAccessible(true); X509Certificate cert = (X509Certificate) f.get(sslCertificate); if (cert == null) { handler.cancel(); } else { try { magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic"); handler.proceed(); } catch (CertificateException exception) { eventBus.post(new CertificateEvent(cert, magicTrustManager, handler)); } } } catch (Exception exception) { handler.cancel(); } } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); } }); webView.loadUrl(baseUrl + "/index.php/login/flow", headers); } private void dispose() { if (userQueryDisposable != null && !userQueryDisposable.isDisposed()) { userQueryDisposable.dispose(); } userQueryDisposable = null; } private void parseAndLoginFromWebView(String dataString) { LoginData loginData = parseLoginData(assembledPrefix, dataString); if (loginData != null) { dispose(); UserEntity currentUser = userUtils.getCurrentUser(); ApplicationWideMessageHolder.MessageType messageType = null; if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(), baseUrl)) { messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED; } if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) { ApplicationWideMessageHolder.getInstance().setMessageType( ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION); if (!isPasswordUpdate) { getRouter().popToRoot(); } else { getRouter().popCurrentController(); } } ApplicationWideMessageHolder.MessageType finalMessageType = messageType; cookieManager.getCookieStore().removeAll(); if (!isPasswordUpdate && finalMessageType == null) { Bundle bundle = new Bundle(); bundle.putString(BundleKeys.KEY_USERNAME, loginData.getUsername()); bundle.putString(BundleKeys.KEY_TOKEN, loginData.getToken()); bundle.putString(BundleKeys.KEY_BASE_URL, loginData.getServerUrl()); String protocol = ""; if (baseUrl.startsWith("http://")) { protocol = "http://"; } else if (baseUrl.startsWith("https://")) { protocol = "https://"; } if (!TextUtils.isEmpty(protocol)) { bundle.putString(BundleKeys.KEY_ORIGINAL_PROTOCOL, protocol); } getRouter().pushController(RouterTransaction.with(new AccountVerificationController (bundle)).pushChangeHandler(new HorizontalChangeHandler()) .popChangeHandler(new HorizontalChangeHandler())); } else { if (isPasswordUpdate) { if (currentUser != null) { userQueryDisposable = userUtils.createOrUpdateUser(null, loginData.getToken(), null, null, null, true, null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(userEntity -> { if (finalMessageType != null) { ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType); } getRouter().popCurrentController(); }, throwable -> dispose(), this::dispose); } } else { if (finalMessageType != null) { ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType); } getRouter().popToRoot(); } } } } private LoginData parseLoginData(String prefix, String dataString) { if (dataString.length() < prefix.length()) { return null; } LoginData loginData = new LoginData(); // format is xxx://login/server:xxx&user:xxx&password:xxx String data = dataString.substring(prefix.length()); String[] values = data.split("&"); if (values.length != 3) { return null; } for (String value : values) { if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { loginData.setUsername(URLDecoder.decode( value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { loginData.setToken(URLDecoder.decode( value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { loginData.setServerUrl(URLDecoder.decode( value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); } else { return null; } } if (!TextUtils.isEmpty(loginData.getServerUrl()) && !TextUtils.isEmpty(loginData.getUsername()) && !TextUtils.isEmpty(loginData.getToken())) { return loginData; } else { return null; } } @Override public void onDestroy() { super.onDestroy(); dispose(); } @Override protected void onDestroyView(@NonNull View view) { super.onDestroyView(view); if (getActivity() != null) { getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR); } } }