2017-10-23 11:40:38 +01:00
|
|
|
/*
|
|
|
|
*
|
|
|
|
* Nextcloud Talk application
|
|
|
|
*
|
|
|
|
* @author Mario Danic
|
2017-10-24 18:11:30 +01:00
|
|
|
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
|
2017-10-23 11:40:38 +01:00
|
|
|
*
|
|
|
|
* 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.content.pm.ActivityInfo;
|
2017-10-27 22:10:06 +01:00
|
|
|
import android.net.http.SslCertificate;
|
2017-10-23 11:40:38 +01:00
|
|
|
import android.net.http.SslError;
|
|
|
|
import android.os.Bundle;
|
2017-12-21 02:53:26 +00:00
|
|
|
import android.security.KeyChain;
|
|
|
|
import android.security.KeyChainException;
|
2017-10-23 11:40:38 +01:00
|
|
|
import android.support.annotation.NonNull;
|
|
|
|
import android.text.TextUtils;
|
2017-12-21 02:53:26 +00:00
|
|
|
import android.util.Log;
|
2017-10-23 11:40:38 +01:00
|
|
|
import android.view.LayoutInflater;
|
|
|
|
import android.view.View;
|
|
|
|
import android.view.ViewGroup;
|
2017-12-21 02:53:26 +00:00
|
|
|
import android.webkit.ClientCertRequest;
|
2017-11-06 19:01:53 +00:00
|
|
|
import android.webkit.CookieSyncManager;
|
2017-10-23 11:40:38 +01:00
|
|
|
import android.webkit.SslErrorHandler;
|
2017-12-07 16:17:44 +00:00
|
|
|
import android.webkit.WebSettings;
|
2017-10-23 11:40:38 +01:00
|
|
|
import android.webkit.WebView;
|
|
|
|
import android.webkit.WebViewClient;
|
2017-10-26 19:50:34 +01:00
|
|
|
import android.widget.ProgressBar;
|
2017-10-23 11:40:38 +01:00
|
|
|
|
|
|
|
import com.bluelinelabs.conductor.RouterTransaction;
|
2017-10-23 19:33:14 +01:00
|
|
|
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
|
2017-10-23 11:40:38 +01:00
|
|
|
import com.nextcloud.talk.R;
|
|
|
|
import com.nextcloud.talk.api.helpers.api.ApiHelper;
|
|
|
|
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
|
|
|
import com.nextcloud.talk.controllers.base.BaseController;
|
2017-10-29 22:08:59 +00:00
|
|
|
import com.nextcloud.talk.events.CertificateEvent;
|
2017-10-23 11:40:38 +01:00
|
|
|
import com.nextcloud.talk.models.LoginData;
|
2017-10-28 19:20:52 +01:00
|
|
|
import com.nextcloud.talk.persistence.entities.UserEntity;
|
2017-11-07 14:41:14 +00:00
|
|
|
import com.nextcloud.talk.utils.ErrorMessageHolder;
|
2017-10-25 20:38:16 +01:00
|
|
|
import com.nextcloud.talk.utils.bundle.BundleBuilder;
|
|
|
|
import com.nextcloud.talk.utils.bundle.BundleKeys;
|
2017-10-23 11:40:38 +01:00
|
|
|
import com.nextcloud.talk.utils.database.user.UserUtils;
|
2017-10-27 22:10:06 +01:00
|
|
|
import com.nextcloud.talk.utils.ssl.MagicTrustManager;
|
2017-10-29 22:08:59 +00:00
|
|
|
|
|
|
|
import org.greenrobot.eventbus.EventBus;
|
2017-10-23 11:40:38 +01:00
|
|
|
|
2017-10-27 22:10:06 +01:00
|
|
|
import java.lang.reflect.Field;
|
2017-12-21 02:53:26 +00:00
|
|
|
import java.net.MalformedURLException;
|
|
|
|
import java.net.URL;
|
2017-10-23 11:40:38 +01:00
|
|
|
import java.net.URLDecoder;
|
2017-12-21 02:53:26 +00:00
|
|
|
import java.security.PrivateKey;
|
2017-10-27 22:10:06 +01:00
|
|
|
import java.security.cert.CertificateException;
|
|
|
|
import java.security.cert.X509Certificate;
|
2017-10-23 11:40:38 +01:00
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
import javax.inject.Inject;
|
|
|
|
|
|
|
|
import autodagger.AutoInjector;
|
|
|
|
import butterknife.BindView;
|
|
|
|
import io.reactivex.disposables.Disposable;
|
|
|
|
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
|
|
|
|
ReactiveEntityStore<Persistable> dataStore;
|
2017-10-27 22:10:06 +01:00
|
|
|
@Inject
|
|
|
|
MagicTrustManager magicTrustManager;
|
2017-11-06 22:43:11 +00:00
|
|
|
@Inject
|
|
|
|
EventBus eventBus;
|
2017-11-28 19:45:45 +00:00
|
|
|
@Inject
|
|
|
|
java.net.CookieManager cookieManager;
|
2017-10-23 11:40:38 +01:00
|
|
|
|
|
|
|
@BindView(R.id.webview)
|
|
|
|
WebView webView;
|
|
|
|
|
2017-10-26 19:50:34 +01:00
|
|
|
@BindView(R.id.progress_bar)
|
|
|
|
ProgressBar progressBar;
|
|
|
|
|
2017-10-23 11:40:38 +01:00
|
|
|
private String assembledPrefix;
|
|
|
|
|
|
|
|
private Disposable userQueryDisposable;
|
|
|
|
|
|
|
|
private String baseUrl;
|
2017-10-26 19:50:34 +01:00
|
|
|
private boolean isPasswordUpdate;
|
2017-10-23 11:40:38 +01:00
|
|
|
|
2017-10-26 19:50:34 +01:00
|
|
|
public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) {
|
2017-10-23 11:40:38 +01:00
|
|
|
this.baseUrl = baseUrl;
|
2017-10-26 19:50:34 +01:00
|
|
|
this.isPasswordUpdate = isPasswordUpdate;
|
2017-10-23 11:40:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public WebViewLoginController(Bundle args) {
|
|
|
|
super(args);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
|
|
|
|
return inflater.inflate(R.layout.controller_web_view_login, container, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onViewBound(@NonNull View view) {
|
|
|
|
super.onViewBound(view);
|
|
|
|
NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
|
|
|
|
|
|
|
|
if (getActivity() != null) {
|
|
|
|
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
|
|
|
}
|
|
|
|
|
2017-10-26 19:50:34 +01:00
|
|
|
if (getActionBar() != null) {
|
|
|
|
getActionBar().hide();
|
|
|
|
}
|
|
|
|
|
2017-10-23 11:40:38 +01:00
|
|
|
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(ApiHelper.getUserAgent());
|
|
|
|
webView.getSettings().setSaveFormData(false);
|
|
|
|
webView.getSettings().setSavePassword(false);
|
2017-12-07 16:17:44 +00:00
|
|
|
webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
|
2017-10-23 11:40:38 +01:00
|
|
|
webView.clearCache(true);
|
|
|
|
webView.clearFormData();
|
2017-11-06 19:01:53 +00:00
|
|
|
webView.clearHistory();
|
|
|
|
|
|
|
|
CookieSyncManager.createInstance(getActivity());
|
2017-11-28 19:45:45 +00:00
|
|
|
android.webkit.CookieManager.getInstance().removeAllCookies(null);
|
2017-10-23 11:40:38 +01:00
|
|
|
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
|
|
headers.put("OCS-APIRequest", "true");
|
|
|
|
|
|
|
|
webView.setWebViewClient(new WebViewClient() {
|
2017-10-26 19:50:34 +01:00
|
|
|
private boolean basePageLoaded;
|
2017-10-28 19:20:52 +01:00
|
|
|
|
2017-10-23 11:40:38 +01:00
|
|
|
@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) {
|
2017-10-26 19:50:34 +01:00
|
|
|
if (!basePageLoaded) {
|
2017-10-29 22:08:36 +00:00
|
|
|
if (progressBar != null) {
|
|
|
|
progressBar.setVisibility(View.GONE);
|
|
|
|
}
|
2017-11-06 22:43:11 +00:00
|
|
|
|
|
|
|
if (webView != null) {
|
|
|
|
webView.setVisibility(View.VISIBLE);
|
|
|
|
}
|
2017-10-26 19:50:34 +01:00
|
|
|
basePageLoaded = true;
|
|
|
|
}
|
2017-10-23 11:40:38 +01:00
|
|
|
|
2017-10-26 19:50:34 +01:00
|
|
|
super.onPageFinished(view, url);
|
2017-10-23 11:40:38 +01:00
|
|
|
}
|
|
|
|
|
2017-12-21 02:53:26 +00:00
|
|
|
@Override
|
|
|
|
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
|
|
|
|
String host = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
URL url = new URL(webView.getUrl());
|
|
|
|
host = url.getHost();
|
|
|
|
} catch (MalformedURLException e) {
|
|
|
|
Log.d(TAG, "Failed to create url");
|
|
|
|
}
|
|
|
|
|
|
|
|
KeyChain.choosePrivateKeyAlias(getActivity(), alias -> {
|
|
|
|
try {
|
2018-01-12 00:37:38 +00:00
|
|
|
if (alias != null) {
|
|
|
|
PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), alias);
|
|
|
|
X509Certificate[] certificates = KeyChain.getCertificateChain(getActivity(), alias);
|
|
|
|
request.proceed(privateKey, certificates);
|
|
|
|
} else {
|
|
|
|
request.cancel();
|
|
|
|
}
|
2017-12-21 02:53:26 +00:00
|
|
|
} catch (KeyChainException e) {
|
|
|
|
Log.e(TAG, "Failed to get keys via keychain exception");
|
|
|
|
request.cancel();
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
Log.e(TAG, "Failed to get keys due to interruption");
|
|
|
|
request.cancel();
|
|
|
|
}
|
|
|
|
}, new String[]{"RSA"}, null, host, -1, null);
|
|
|
|
}
|
|
|
|
|
2017-10-23 11:40:38 +01:00
|
|
|
@Override
|
|
|
|
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
|
2017-10-27 22:10:06 +01:00
|
|
|
try {
|
|
|
|
SslCertificate sslCertificate = error.getCertificate();
|
|
|
|
Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate");
|
|
|
|
f.setAccessible(true);
|
2017-10-28 19:20:52 +01:00
|
|
|
X509Certificate cert = (X509Certificate) f.get(sslCertificate);
|
2017-10-27 22:10:06 +01:00
|
|
|
|
|
|
|
if (cert == null) {
|
|
|
|
handler.cancel();
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
magicTrustManager.checkServerTrusted(new X509Certificate[]{cert}, "generic");
|
|
|
|
handler.proceed();
|
|
|
|
} catch (CertificateException exception) {
|
2017-11-06 22:43:11 +00:00
|
|
|
eventBus.post(new CertificateEvent(cert, magicTrustManager, handler));
|
2017-10-27 22:10:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (Exception exception) {
|
|
|
|
handler.cancel();
|
|
|
|
}
|
2017-10-23 11:40:38 +01:00
|
|
|
}
|
|
|
|
|
2017-12-21 02:53:26 +00:00
|
|
|
@Override
|
2017-10-23 11:40:38 +01:00
|
|
|
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();
|
|
|
|
|
2017-10-28 19:20:52 +01:00
|
|
|
UserEntity currentUser = userUtils.getCurrentUser();
|
|
|
|
|
2017-11-07 14:41:14 +00:00
|
|
|
ErrorMessageHolder.ErrorMessageType errorMessageType = null;
|
2017-11-06 22:43:11 +00:00
|
|
|
if (currentUser != null && isPasswordUpdate &&
|
|
|
|
!currentUser.getUsername().equals(loginData.getUsername())) {
|
2017-11-07 14:41:14 +00:00
|
|
|
ErrorMessageHolder.getInstance().setMessageType(
|
|
|
|
ErrorMessageHolder.ErrorMessageType.WRONG_ACCOUNT);
|
2017-11-06 22:43:11 +00:00
|
|
|
getRouter().popToRoot();
|
|
|
|
} else {
|
2017-10-28 19:20:52 +01:00
|
|
|
|
2017-11-06 22:43:11 +00:00
|
|
|
if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(), baseUrl)) {
|
2017-11-07 14:41:14 +00:00
|
|
|
errorMessageType = ErrorMessageHolder.ErrorMessageType.ACCOUNT_UPDATED_NOT_ADDED;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) {
|
|
|
|
ErrorMessageHolder.getInstance().setMessageType(
|
|
|
|
ErrorMessageHolder.ErrorMessageType.ACCOUNT_SCHEDULED_FOR_DELETION);
|
2017-11-11 04:39:01 +00:00
|
|
|
getRouter().popToRoot();
|
2017-11-06 22:43:11 +00:00
|
|
|
}
|
2017-10-29 10:30:24 +00:00
|
|
|
|
2017-11-06 22:43:11 +00:00
|
|
|
// We use the URL user entered because one provided by the server is NOT reliable
|
2017-11-07 14:41:14 +00:00
|
|
|
ErrorMessageHolder.ErrorMessageType finalErrorMessageType = errorMessageType;
|
2017-11-06 22:43:11 +00:00
|
|
|
userQueryDisposable = userUtils.createOrUpdateUser(loginData.getUsername(), loginData.getToken(),
|
2018-01-08 19:27:04 +00:00
|
|
|
loginData.getServerUrl(), null, null, true,
|
|
|
|
null).
|
2017-11-06 22:43:11 +00:00
|
|
|
subscribe(userEntity -> {
|
2017-11-28 19:45:45 +00:00
|
|
|
cookieManager.getCookieStore().removeAll();
|
2017-11-07 14:41:14 +00:00
|
|
|
if (!isPasswordUpdate && finalErrorMessageType == null) {
|
2017-11-06 22:43:11 +00:00
|
|
|
BundleBuilder bundleBuilder = new BundleBuilder(new Bundle());
|
|
|
|
bundleBuilder.putString(BundleKeys.KEY_USERNAME, userEntity.getUsername());
|
|
|
|
bundleBuilder.putString(BundleKeys.KEY_TOKEN, userEntity.getToken());
|
|
|
|
bundleBuilder.putString(BundleKeys.KEY_BASE_URL, userEntity.getBaseUrl());
|
|
|
|
getRouter().pushController(RouterTransaction.with(new AccountVerificationController
|
|
|
|
(bundleBuilder.build())).pushChangeHandler(new HorizontalChangeHandler())
|
2017-11-06 19:01:53 +00:00
|
|
|
.popChangeHandler(new HorizontalChangeHandler()));
|
2017-11-06 22:43:11 +00:00
|
|
|
} else {
|
2017-11-07 14:41:14 +00:00
|
|
|
if (finalErrorMessageType != null) {
|
|
|
|
ErrorMessageHolder.getInstance().setMessageType(finalErrorMessageType);
|
2017-11-06 22:43:11 +00:00
|
|
|
}
|
|
|
|
getRouter().popToRoot();
|
2017-11-06 19:01:53 +00:00
|
|
|
}
|
2017-11-06 22:43:11 +00:00
|
|
|
}, throwable -> dispose(),
|
|
|
|
this::dispose);
|
|
|
|
}
|
2017-10-23 11:40:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private LoginData parseLoginData(String prefix, String dataString) {
|
|
|
|
if (dataString.length() < prefix.length()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
LoginData loginData = new LoginData();
|
|
|
|
|
2017-11-07 12:20:09 +00:00
|
|
|
// format is xxx://login/server1:xxx&user:xxx&password:xxx
|
2017-10-23 11:40:38 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-29 12:44:10 +00:00
|
|
|
@Override
|
|
|
|
public void onDestroy() {
|
|
|
|
super.onDestroy();
|
|
|
|
dispose();
|
|
|
|
}
|
|
|
|
|
2017-10-23 11:40:38 +01:00
|
|
|
@Override
|
|
|
|
protected void onDestroyView(@NonNull View view) {
|
|
|
|
super.onDestroyView(view);
|
|
|
|
if (getActivity() != null) {
|
|
|
|
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|