mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 20:19:42 +01:00
migrate WebViewLoginController to kotlin
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
This commit is contained in:
parent
ed4d27aff0
commit
6b7dd29b07
@ -1,504 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.talk.controllers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Bitmap;
|
||||
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.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
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.jobs.PushRegistrationWorker;
|
||||
import com.nextcloud.talk.models.LoginData;
|
||||
import com.nextcloud.talk.models.database.UserEntity;
|
||||
import com.nextcloud.talk.utils.DisplayUtils;
|
||||
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 androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.OneTimeWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
import autodagger.AutoInjector;
|
||||
import butterknife.BindView;
|
||||
import de.cotech.hw.fido.WebViewFidoBridge;
|
||||
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<Persistable> 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;
|
||||
|
||||
private WebViewFidoBridge webViewFidoBridge;
|
||||
|
||||
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_product_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.Companion.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);
|
||||
|
||||
webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView((AppCompatActivity) getActivity(), webView);
|
||||
|
||||
CookieSyncManager.createInstance(getActivity());
|
||||
android.webkit.CookieManager.getInstance().removeAllCookies(null);
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("OCS-APIRequest", "true");
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
private boolean basePageLoaded;
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
webViewFidoBridge.delegateShouldInterceptRequest(view, request);
|
||||
return super.shouldInterceptRequest(view, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
super.onPageStarted(view, url, favicon);
|
||||
webViewFidoBridge.delegateOnPageStarted(view, url, favicon);
|
||||
}
|
||||
|
||||
@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.INSTANCE.getKEY_USERNAME(), loginData.getUsername());
|
||||
bundle.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), loginData.getToken());
|
||||
bundle.putString(BundleKeys.INSTANCE.getKEY_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.INSTANCE.getKEY_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, "", Boolean.TRUE,
|
||||
null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(userEntity -> {
|
||||
if (finalMessageType != null) {
|
||||
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
|
||||
}
|
||||
|
||||
Data data =
|
||||
new Data.Builder().putString(PushRegistrationWorker.ORIGIN,
|
||||
"WebViewLoginController#parseAndLoginFromWebView").build();
|
||||
OneTimeWorkRequest pushRegistrationWork = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class)
|
||||
.setInputData(data)
|
||||
.build();
|
||||
WorkManager.getInstance().enqueue(pushRegistrationWork);
|
||||
|
||||
getRouter().popCurrentController();
|
||||
}, throwable -> dispose(),
|
||||
this::dispose);
|
||||
}
|
||||
} else {
|
||||
if (finalMessageType != null) {
|
||||
// FIXME when the user registers a new account that was setup before (aka
|
||||
// ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED)
|
||||
// The token is not updated in the database and therefor the account not visible/usable
|
||||
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
|
||||
protected void onAttach(@NonNull View view) {
|
||||
super.onAttach(view);
|
||||
|
||||
if (getActivity() != null && getResources() != null) {
|
||||
DisplayUtils.applyColorToStatusBar(getActivity(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));
|
||||
DisplayUtils.applyColorToNavigationBar(getActivity().getWindow(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppBarLayoutType getAppBarLayoutType() {
|
||||
return AppBarLayoutType.EMPTY;
|
||||
}
|
||||
}
|
@ -0,0 +1,473 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* @author Andy Scherzinger
|
||||
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.talk.controllers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Bitmap
|
||||
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.View
|
||||
import android.webkit.ClientCertRequest
|
||||
import android.webkit.CookieSyncManager
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.work.Data
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import autodagger.AutoInjector
|
||||
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.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||
import com.nextcloud.talk.controllers.base.NewBaseController
|
||||
import com.nextcloud.talk.controllers.util.viewBinding
|
||||
import com.nextcloud.talk.databinding.ControllerWebViewLoginBinding
|
||||
import com.nextcloud.talk.events.CertificateEvent
|
||||
import com.nextcloud.talk.jobs.PushRegistrationWorker
|
||||
import com.nextcloud.talk.models.LoginData
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
|
||||
import com.nextcloud.talk.utils.database.user.UserUtils
|
||||
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
|
||||
import com.nextcloud.talk.utils.ssl.MagicTrustManager
|
||||
import de.cotech.hw.fido.WebViewFidoBridge
|
||||
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
|
||||
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 javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class WebViewLoginController(args: Bundle? = null) : NewBaseController(
|
||||
R.layout.controller_web_view_login,
|
||||
args
|
||||
) {
|
||||
private val binding: ControllerWebViewLoginBinding by viewBinding(ControllerWebViewLoginBinding::bind)
|
||||
|
||||
@Inject
|
||||
lateinit var userUtils: UserUtils
|
||||
|
||||
@Inject
|
||||
lateinit var dataStore: ReactiveEntityStore<Persistable>
|
||||
|
||||
@Inject
|
||||
lateinit var magicTrustManager: MagicTrustManager
|
||||
|
||||
@Inject
|
||||
lateinit var eventBus: EventBus
|
||||
|
||||
@Inject
|
||||
lateinit var cookieManager: CookieManager
|
||||
|
||||
private var assembledPrefix: String? = null
|
||||
private var userQueryDisposable: Disposable? = null
|
||||
private var baseUrl: String? = null
|
||||
private var isPasswordUpdate = false
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
private var loginStep = 0
|
||||
private var automatedLoginAttempted = false
|
||||
private var webViewFidoBridge: WebViewFidoBridge? = null
|
||||
|
||||
constructor(baseUrl: String?, isPasswordUpdate: Boolean) : this() {
|
||||
this.baseUrl = baseUrl
|
||||
this.isPasswordUpdate = isPasswordUpdate
|
||||
}
|
||||
|
||||
constructor(baseUrl: String?, isPasswordUpdate: Boolean, username: String?, password: String?) : this() {
|
||||
this.baseUrl = baseUrl
|
||||
this.isPasswordUpdate = isPasswordUpdate
|
||||
this.username = username
|
||||
this.password = password
|
||||
}
|
||||
|
||||
private val webLoginUserAgent: String
|
||||
get() = (
|
||||
Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault())
|
||||
+ Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault())
|
||||
+ " "
|
||||
+ Build.MODEL
|
||||
+ " ("
|
||||
+ resources!!.getString(R.string.nc_app_product_name) + ")"
|
||||
)
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewBound(view: View) {
|
||||
super.onViewBound(view)
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
|
||||
actionBar?.hide()
|
||||
|
||||
assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
|
||||
binding.webview.settings.allowFileAccess = false
|
||||
binding.webview.settings.allowFileAccessFromFileURLs = false
|
||||
binding.webview.settings.javaScriptEnabled = true
|
||||
binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false
|
||||
binding.webview.settings.domStorageEnabled = true
|
||||
binding.webview.settings.setUserAgentString(webLoginUserAgent)
|
||||
binding.webview.settings.saveFormData = false
|
||||
binding.webview.settings.savePassword = false
|
||||
binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
|
||||
binding.webview.clearCache(true)
|
||||
binding.webview.clearFormData()
|
||||
binding.webview.clearHistory()
|
||||
WebView.clearClientCertPreferences(null)
|
||||
webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, binding.webview)
|
||||
CookieSyncManager.createInstance(activity)
|
||||
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||
val headers: MutableMap<String, String> = HashMap()
|
||||
headers.put("OCS-APIRequest", "true")
|
||||
binding.webview.webViewClient = object : WebViewClient() {
|
||||
private var basePageLoaded = false
|
||||
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
|
||||
webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
webViewFidoBridge?.delegateOnPageStarted(view, url, favicon)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
if (url.startsWith(assembledPrefix!!)) {
|
||||
parseAndLoginFromWebView(url)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
loginStep++
|
||||
if (!basePageLoaded) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.webview.visibility = View.VISIBLE
|
||||
|
||||
basePageLoaded = true
|
||||
}
|
||||
if (!TextUtils.isEmpty(username)) {
|
||||
if (loginStep == 1) {
|
||||
binding.webview.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };")
|
||||
} else if (!automatedLoginAttempted) {
|
||||
automatedLoginAttempted = true
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
binding.webview.loadUrl(
|
||||
"javascript:var justStore = document.getElementById('user').value = '$username';"
|
||||
)
|
||||
} else {
|
||||
binding.webview.loadUrl(
|
||||
"javascript: {" +
|
||||
"document.getElementById('user').value = '" + username + "';" +
|
||||
"document.getElementById('password').value = '" + password + "';" +
|
||||
"document.getElementById('submit').click(); };"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
|
||||
override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) {
|
||||
val userEntity = userUtils.currentUser
|
||||
var alias: String? = null
|
||||
if (!isPasswordUpdate) {
|
||||
alias = appPreferences!!.temporaryClientCertAlias
|
||||
}
|
||||
if (TextUtils.isEmpty(alias) && userEntity != null) {
|
||||
alias = userEntity.clientCertificate
|
||||
}
|
||||
if (!TextUtils.isEmpty(alias)) {
|
||||
val finalAlias = alias
|
||||
Thread {
|
||||
try {
|
||||
val privateKey = KeyChain.getPrivateKey(activity!!, finalAlias!!)
|
||||
val certificates = KeyChain.getCertificateChain(
|
||||
activity!!, finalAlias
|
||||
)
|
||||
if (privateKey != null && certificates != null) {
|
||||
request.proceed(privateKey, certificates)
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
} catch (e: KeyChainException) {
|
||||
request.cancel()
|
||||
} catch (e: InterruptedException) {
|
||||
request.cancel()
|
||||
}
|
||||
}.start()
|
||||
} else {
|
||||
KeyChain.choosePrivateKeyAlias(activity!!, { chosenAlias: String? ->
|
||||
if (chosenAlias != null) {
|
||||
appPreferences!!.temporaryClientCertAlias = chosenAlias
|
||||
Thread {
|
||||
var privateKey: PrivateKey? = null
|
||||
try {
|
||||
privateKey = KeyChain.getPrivateKey(activity!!, chosenAlias)
|
||||
val certificates = KeyChain.getCertificateChain(
|
||||
activity!!, chosenAlias
|
||||
)
|
||||
if (privateKey != null && certificates != null) {
|
||||
request.proceed(privateKey, certificates)
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
} catch (e: KeyChainException) {
|
||||
request.cancel()
|
||||
} catch (e: InterruptedException) {
|
||||
request.cancel()
|
||||
}
|
||||
}.start()
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
}, arrayOf("RSA", "EC"), null, request.host, request.port, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
|
||||
try {
|
||||
val sslCertificate = error.certificate
|
||||
val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate")
|
||||
f.isAccessible = true
|
||||
val cert = f[sslCertificate] as X509Certificate
|
||||
if (cert == null) {
|
||||
handler.cancel()
|
||||
} else {
|
||||
try {
|
||||
magicTrustManager.checkServerTrusted(arrayOf(cert), "generic")
|
||||
handler.proceed()
|
||||
} catch (exception: CertificateException) {
|
||||
eventBus.post(CertificateEvent(cert, magicTrustManager, handler))
|
||||
}
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
handler.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
}
|
||||
}
|
||||
binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
|
||||
}
|
||||
|
||||
private fun dispose() {
|
||||
if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) {
|
||||
userQueryDisposable!!.dispose()
|
||||
}
|
||||
userQueryDisposable = null
|
||||
}
|
||||
|
||||
private fun parseAndLoginFromWebView(dataString: String) {
|
||||
val loginData = parseLoginData(assembledPrefix, dataString)
|
||||
if (loginData != null) {
|
||||
dispose()
|
||||
val currentUser = userUtils.currentUser
|
||||
var messageType: ApplicationWideMessageHolder.MessageType? = null
|
||||
if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.username, baseUrl)) {
|
||||
messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED
|
||||
}
|
||||
if (userUtils.checkIfUserIsScheduledForDeletion(loginData.username, baseUrl)) {
|
||||
ApplicationWideMessageHolder.getInstance().setMessageType(
|
||||
ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
|
||||
)
|
||||
if (!isPasswordUpdate) {
|
||||
router.popToRoot()
|
||||
} else {
|
||||
router.popCurrentController()
|
||||
}
|
||||
}
|
||||
val finalMessageType = messageType
|
||||
cookieManager.cookieStore.removeAll()
|
||||
if (!isPasswordUpdate && finalMessageType == null) {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_USERNAME, loginData.username)
|
||||
bundle.putString(KEY_TOKEN, loginData.token)
|
||||
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
|
||||
var protocol = ""
|
||||
if (baseUrl!!.startsWith("http://")) {
|
||||
protocol = "http://"
|
||||
} else if (baseUrl!!.startsWith("https://")) {
|
||||
protocol = "https://"
|
||||
}
|
||||
if (!TextUtils.isEmpty(protocol)) {
|
||||
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
|
||||
}
|
||||
router.pushController(
|
||||
RouterTransaction.with(AccountVerificationController(bundle))
|
||||
.pushChangeHandler(HorizontalChangeHandler())
|
||||
.popChangeHandler(HorizontalChangeHandler())
|
||||
)
|
||||
} else {
|
||||
if (isPasswordUpdate) {
|
||||
if (currentUser != null) {
|
||||
userQueryDisposable = userUtils.createOrUpdateUser(
|
||||
null,
|
||||
loginData.token,
|
||||
null,
|
||||
null,
|
||||
"",
|
||||
java.lang.Boolean.TRUE,
|
||||
null,
|
||||
currentUser.id,
|
||||
null,
|
||||
appPreferences!!.temporaryClientCertAlias,
|
||||
null
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
if (finalMessageType != null) {
|
||||
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType)
|
||||
}
|
||||
val data = Data.Builder().putString(
|
||||
PushRegistrationWorker.ORIGIN,
|
||||
"WebViewLoginController#parseAndLoginFromWebView"
|
||||
).build()
|
||||
val pushRegistrationWork = OneTimeWorkRequest.Builder(
|
||||
PushRegistrationWorker::class.java
|
||||
)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(pushRegistrationWork)
|
||||
router.popCurrentController()
|
||||
},
|
||||
{ dispose() }
|
||||
) { dispose() }
|
||||
}
|
||||
} else {
|
||||
if (finalMessageType != null) {
|
||||
// FIXME when the user registers a new account that was setup before (aka
|
||||
// ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED)
|
||||
// The token is not updated in the database and therefor the account not visible/usable
|
||||
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType)
|
||||
}
|
||||
router.popToRoot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLoginData(prefix: String?, dataString: String): LoginData? {
|
||||
if (dataString.length < prefix!!.length) {
|
||||
return null
|
||||
}
|
||||
val loginData = LoginData()
|
||||
|
||||
// format is xxx://login/server:xxx&user:xxx&password:xxx
|
||||
val data: String = dataString.substring(prefix.length)
|
||||
val values: Array<String> = data.split("&").toTypedArray()
|
||||
if (values.size != 3) {
|
||||
return null
|
||||
}
|
||||
for (value in values) {
|
||||
if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
|
||||
loginData.username = URLDecoder.decode(
|
||||
value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
|
||||
)
|
||||
} else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
|
||||
loginData.token = URLDecoder.decode(
|
||||
value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
|
||||
)
|
||||
} else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
|
||||
loginData.serverUrl = URLDecoder.decode(
|
||||
value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length)
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return if (!TextUtils.isEmpty(loginData.serverUrl) && !TextUtils.isEmpty(loginData.username) &&
|
||||
!TextUtils.isEmpty(loginData.token)
|
||||
) {
|
||||
loginData
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
if (activity != null && resources != null) {
|
||||
DisplayUtils.applyColorToStatusBar(
|
||||
activity,
|
||||
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
|
||||
)
|
||||
DisplayUtils.applyColorToNavigationBar(
|
||||
activity!!.window,
|
||||
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
dispose()
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|
||||
}
|
||||
|
||||
init {
|
||||
sharedApplication!!.componentApplication.inject(this)
|
||||
}
|
||||
|
||||
override val appBarLayoutType: AppBarLayoutType
|
||||
get() = AppBarLayoutType.EMPTY
|
||||
|
||||
companion object {
|
||||
const val TAG = "WebViewLoginController"
|
||||
private const val PROTOCOL_SUFFIX = "://"
|
||||
private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user