From 91ab175ed1971b1be9954842d53324c7b6a6e656 Mon Sep 17 00:00:00 2001 From: Marten Rebane <54431068+martenrebane@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:04:36 +0300 Subject: [PATCH] Added proxy functionality --- app/build.gradle | 1 + .../main/diagnostics/DiagnosticsView.java | 59 +++- .../main/settings/SettingsDataStore.java | 115 ++++++++ .../android/main/settings/SettingsView.java | 15 +- .../settings/proxy/SettingsProxyScreen.java | 44 +++ .../settings/proxy/SettingsProxyView.java | 256 ++++++++++++++++++ .../signature/update/SignatureAddSource.java | 9 +- .../update/mobileid/MobileIdOnSubscribe.java | 25 +- .../update/smartid/SmartIdOnSubscribe.java | 28 +- app/src/main/res/layout/main_settings.xml | 29 +- .../main/res/layout/main_settings_proxy.xml | 208 ++++++++++++++ app/src/main/res/values-et/strings.xml | 9 + app/src/main/res/values-ru/strings.xml | 9 + app/src/main/res/values/ids.xml | 14 +- app/src/main/res/values/strings.xml | 9 + build.gradle | 1 + .../ee/ria/DigiDoc/common/ManualProxy.java | 47 ++++ .../ee/ria/DigiDoc/common/ProxyConfig.java | 27 ++ .../ee/ria/DigiDoc/common/ProxySetting.java | 7 + .../java/ee/ria/DigiDoc/common/ProxyUtil.java | 71 +++++ .../src/main/res/values/donottranslate.xml | 10 + configuration-lib/build.gradle | 1 + .../configuration/ConfigurationConstants.java | 6 + .../configuration/ConfigurationManager.java | 2 +- .../loader/CentralConfigurationClient.java | 107 ++++++-- .../loader/CentralConfigurationLoader.java | 6 +- ...tchAndPackageDefaultConfigurationTask.java | 2 +- .../mobileid/rest/ServiceGenerator.java | 29 +- .../mobileid/service/MobileSignConstants.java | 6 + .../mobileid/service/MobileSignService.java | 34 ++- .../smartid/rest/ServiceGenerator.java | 27 +- .../smartid/service/SmartSignConstants.java | 6 + .../smartid/service/SmartSignService.java | 33 ++- 33 files changed, 1187 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyScreen.java create mode 100644 app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyView.java create mode 100644 app/src/main/res/layout/main_settings_proxy.xml create mode 100644 common-lib/src/main/java/ee/ria/DigiDoc/common/ManualProxy.java create mode 100644 common-lib/src/main/java/ee/ria/DigiDoc/common/ProxyConfig.java create mode 100644 common-lib/src/main/java/ee/ria/DigiDoc/common/ProxySetting.java create mode 100644 common-lib/src/main/java/ee/ria/DigiDoc/common/ProxyUtil.java diff --git a/app/build.gradle b/app/build.gradle index 25f25ca41..5d4b3602c 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,6 +95,7 @@ dependencies { implementation "androidx.cardview:cardview:${androidxCardviewVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" implementation "androidx.work:work-runtime:${androidxWorkRuntime}" + implementation "androidx.security:security-crypto:${androidxSecurityCrypto}" implementation "com.takisoft.preferencex:preferencex:${preferencexVersion}" diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java index 97f3504e9..64dcc655e 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java @@ -35,6 +35,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; +import java.net.Proxy; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.text.Normalizer; @@ -43,12 +44,14 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.concurrent.TimeUnit; import ee.ria.DigiDoc.BuildConfig; import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; +import ee.ria.DigiDoc.android.main.settings.SettingsDataStore; import ee.ria.DigiDoc.android.utils.ClickableDialogUtil; import ee.ria.DigiDoc.android.utils.TSLException; import ee.ria.DigiDoc.android.utils.TSLUtil; @@ -59,6 +62,10 @@ import ee.ria.DigiDoc.android.utils.navigator.Transaction; import ee.ria.DigiDoc.android.utils.widget.ConfirmationDialog; import ee.ria.DigiDoc.common.FileUtil; +import ee.ria.DigiDoc.common.ManualProxy; +import ee.ria.DigiDoc.common.ProxyConfig; +import ee.ria.DigiDoc.common.ProxySetting; +import ee.ria.DigiDoc.common.ProxyUtil; import ee.ria.DigiDoc.configuration.ConfigurationDateUtil; import ee.ria.DigiDoc.configuration.ConfigurationManagerService; import ee.ria.DigiDoc.configuration.ConfigurationProvider; @@ -67,17 +74,22 @@ import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; +import okhttp3.Authenticator; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import okhttp3.internal.tls.OkHostnameVerifier; import timber.log.Timber; public final class DiagnosticsView extends CoordinatorLayout implements ContentView { + private static final int DEFAULT_TIMEOUT = 5; + private final Navigator navigator; private final SimpleDateFormat dateFormat; private final Toolbar toolbarView; private final ConfirmationDialog diagnosticsRestartConfirmationDialog; + private final SettingsDataStore settingsDataStore; private final ViewDisposables disposables; @@ -96,6 +108,7 @@ public DiagnosticsView(Context context) { toolbarView = findViewById(R.id.toolbar); View saveDiagnosticsButton = findViewById(R.id.configurationSaveButton); navigator = ApplicationApp.component(context).navigator(); + settingsDataStore = ApplicationApp.component(context).settingsDataStore(); ContentView.addInvisibleElement(getContext(), this); @@ -103,7 +116,7 @@ public DiagnosticsView(Context context) { R.string.main_diagnostics_restart_message, R.id.mainDiagnosticsRestartConfirmationDialog); SwitchCompat activateLogFileGenerating = findViewById(R.id.mainDiagnosticsLogging); - activateLogFileGenerating.setChecked(((Activity) this.getContext()).getSettingsDataStore().getIsLogFileGenerationEnabled()); + activateLogFileGenerating.setChecked(settingsDataStore.getIsLogFileGenerationEnabled()); Button saveLogFileButton = findViewById(R.id.mainDiagnosticsSaveLoggingButton); saveLogFileButton.setVisibility( (activateLogFileGenerating.isChecked() && @@ -134,14 +147,14 @@ public DiagnosticsView(Context context) { private void fileLogToggleListener(SwitchCompat activateLogFileGenerating) { activateLogFileGenerating.setOnCheckedChangeListener((buttonView, isChecked) -> { Activity activityContext = ((Activity) this.getContext()); - boolean isLogFileGenerationEnabled = activityContext.getSettingsDataStore().getIsLogFileGenerationEnabled(); + boolean isLogFileGenerationEnabled = settingsDataStore.getIsLogFileGenerationEnabled(); if (isChecked) { diagnosticsRestartConfirmationDialog.show(); ClickableDialogUtil.makeLinksInDialogClickable(diagnosticsRestartConfirmationDialog); diagnosticsRestartConfirmationDialog.positiveButtonClicks() .doOnNext(next -> { diagnosticsRestartConfirmationDialog.dismiss(); - activityContext.getSettingsDataStore().setIsLogFileGenerationEnabled(true); + settingsDataStore.setIsLogFileGenerationEnabled(true); activityContext.restartAppWithIntent(activityContext.getIntent(), true); }) .subscribe(); @@ -149,12 +162,12 @@ private void fileLogToggleListener(SwitchCompat activateLogFileGenerating) { .doOnNext(next -> { diagnosticsRestartConfirmationDialog.dismiss(); activateLogFileGenerating.setChecked(false); - activityContext.getSettingsDataStore().setIsLogFileGenerationEnabled(false); + settingsDataStore.setIsLogFileGenerationEnabled(false); }) .subscribe(); } else { - activityContext.getSettingsDataStore().setIsLogFileGenerationEnabled(false); - activityContext.getSettingsDataStore().setIsLogFileGenerationRunning(false); + settingsDataStore.setIsLogFileGenerationEnabled(false); + settingsDataStore.setIsLogFileGenerationRunning(false); if (isLogFileGenerationEnabled) { activityContext.restartAppWithIntent(activityContext.getIntent(), true); } @@ -354,7 +367,7 @@ private void setData(ConfigurationProvider configurationProvider) { } private void appendTslVersion(TextView tslUrlTextView, String tslUrl) { - tslVersionDisposable = getObservableTslVersion(tslUrl ) + tslVersionDisposable = getObservableTslVersion(tslUrl) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -363,9 +376,35 @@ private void appendTslVersion(TextView tslUrlTextView, String tslUrl) { ); } + private ProxySetting getProxySetting() { + return settingsDataStore.getProxySetting(); + } + + private ManualProxy getManualProxySettings() { + return new ManualProxy( + settingsDataStore.getProxyHost(), + settingsDataStore.getProxyPort(), + settingsDataStore.getProxyUsername(), + settingsDataStore.getProxyPassword(navigator.activity()) + ); + } + private Observable getObservableTslVersion(String tslUrl) { + ProxySetting proxySetting = getProxySetting(); + boolean isProxySSLEnabled = settingsDataStore.getIsProxyForSSLEnabled(); + boolean useHTTPSProxy = ProxyUtil.useHTTPSProxy(isProxySSLEnabled, getManualProxySettings()); + ProxyConfig proxyConfig = ProxyUtil.getProxy(proxySetting, getManualProxySettings()); + return Observable.fromCallable(() -> { - OkHttpClient client = new OkHttpClient(); + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .proxy(proxySetting == ProxySetting.NO_PROXY || !useHTTPSProxy ? Proxy.NO_PROXY : proxyConfig.proxy()) + .proxyAuthenticator(proxySetting == ProxySetting.NO_PROXY || !useHTTPSProxy ? Authenticator.NONE : proxyConfig.authenticator()) + .hostnameVerifier(OkHostnameVerifier.INSTANCE) + .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + .callTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) + .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS); + OkHttpClient client = builder.build(); Request request = new Request.Builder().url(tslUrl).build(); Response response = client.newCall(request).execute(); if (response.isSuccessful() && response.body() != null) { @@ -381,7 +420,7 @@ private Observable getObservableTslVersion(String tslUrl) { } private String getRpUuidText() { - String rpUuid = ((Activity) this.getContext()).getSettingsDataStore().getUuid(); + String rpUuid = settingsDataStore.getUuid(); int uuid = rpUuid == null || rpUuid.isEmpty() ? R.string.main_diagnostics_rpuuid_default : R.string.main_diagnostics_rpuuid_custom; @@ -389,7 +428,7 @@ private String getRpUuidText() { } private String getTsaUrlText() { - return ((Activity) this.getContext()).getSettingsDataStore().getTsaUrl(); + return settingsDataStore.getTsaUrl(); } private void setTslCacheData() { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java index ca67c83b3..6baa57570 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java @@ -1,12 +1,20 @@ package ee.ria.DigiDoc.android.main.settings; +import static ee.ria.DigiDoc.common.ProxySetting.NO_PROXY; + import android.app.Application; +import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; +import android.util.Log; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKeys; +import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -14,16 +22,23 @@ import javax.inject.Inject; import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.utils.ToastUtil; +import ee.ria.DigiDoc.common.ManualProxy; +import ee.ria.DigiDoc.common.ProxySetting; +import timber.log.Timber; public final class SettingsDataStore { private static final String KEY_LOCALE = "locale"; + private static final String ENCRYPTED_PREFERENCES_KEY = "encryptedPreferencesStorage"; private final SharedPreferences preferences; + private final SharedPreferences encryptedPreferences; private final Resources resources; @Inject SettingsDataStore(Application application) { preferences = PreferenceManager.getDefaultSharedPreferences(application); + encryptedPreferences = getEncryptedPreferences(application.getApplicationContext()); this.resources = application.getResources(); } @@ -277,4 +292,104 @@ public void setIsTsaCertificateViewVisible(boolean isVisible) { public boolean getIsTsaCertificateViewVisible() { return preferences.getBoolean(resources.getString(R.string.main_settings_tsa_cert_view), false); } + + public void setProxySetting(ProxySetting proxySetting) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(resources.getString(R.string.main_settings_proxy_setting_key), proxySetting.name()); + editor.commit(); + } + + public ProxySetting getProxySetting() { + String settingKey = preferences.getString(resources.getString(R.string.main_settings_proxy_setting_key), NO_PROXY.name()); + try { + return ProxySetting.valueOf(settingKey); + } catch (IllegalArgumentException iae) { + Timber.log(Log.ERROR, iae, "Unable to get proxy setting"); + return NO_PROXY; + } + } + + public void setIsProxyForSSLEnabled(boolean isEnabled) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(resources.getString(R.string.main_settings_proxy_ssl_enabled_key), isEnabled); + editor.commit(); + } + + public boolean getIsProxyForSSLEnabled() { + return preferences.getBoolean(resources.getString(R.string.main_settings_proxy_ssl_enabled_key), true); + } + + public void setProxyHost(String host) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(resources.getString(R.string.main_settings_proxy_host_key), host); + editor.commit(); + } + + public String getProxyHost() { + return preferences.getString(resources.getString(R.string.main_settings_proxy_host_key), ""); + } + + public void setProxyPort(int port) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putInt(resources.getString(R.string.main_settings_proxy_port_key), port); + editor.commit(); + } + + public int getProxyPort() { + return preferences.getInt(resources.getString(R.string.main_settings_proxy_port_key), 80); + } + + public void setProxyUsername(String username) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(resources.getString(R.string.main_settings_proxy_username_key), username); + editor.commit(); + } + + public String getProxyUsername() { + return preferences.getString(resources.getString(R.string.main_settings_proxy_username_key), ""); + } + + public void setProxyPassword(Context context, String password) { + SharedPreferences encryptedPreferences = getEncryptedPreferences(context); + if (encryptedPreferences != null) { + SharedPreferences.Editor editor = encryptedPreferences.edit(); + editor.putString(resources.getString(R.string.main_settings_proxy_password_key), password); + editor.commit(); + } + Timber.log(Log.ERROR, "Unable to set proxy password"); + } + + public String getProxyPassword(Context context) { + SharedPreferences encryptedPreferences = getEncryptedPreferences(context); + if (encryptedPreferences != null) { + return encryptedPreferences.getString(resources.getString(R.string.main_settings_proxy_password_key), ""); + } + Timber.log(Log.ERROR, "Unable to get proxy password"); + return ""; + } + + public ManualProxy getManualProxySettings(Context context) { + return new ManualProxy(getProxyHost(), getProxyPort(), + getProxyUsername(), getProxyPassword(context)); + } + + @Nullable + private static SharedPreferences getEncryptedPreferences(Context context) { + String masterKey; + try { + masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC); + return EncryptedSharedPreferences.create( + ENCRYPTED_PREFERENCES_KEY, + masterKey, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ); + + } catch (GeneralSecurityException | IOException e) { + Timber.log(Log.ERROR, e, "Unable to get encrypted preferences"); + ToastUtil.showError(context, R.string.signature_update_mobile_id_error_general_client); + return null; + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java index 1642677f0..66cf0ca88 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java @@ -15,27 +15,24 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.main.settings.access.SettingsAccessScreen; +import ee.ria.DigiDoc.android.main.settings.proxy.SettingsProxyScreen; import ee.ria.DigiDoc.android.main.settings.role.SettingsRoleAndAddressScreen; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.disposables.Disposable; -import kotlin.Unit; public final class SettingsView extends CoordinatorLayout implements ContentView { private final Toolbar toolbarView; + private final Button accessCategory; + private final Button proxyCategory; + private final Button roleAndAddressCategory; private final Navigator navigator; private final ViewDisposables disposables; - private final Button accessCategory; - private final Button roleAndAddressCategory; - public SettingsView(Context context) { this(context, null); } @@ -53,6 +50,7 @@ public SettingsView(Context context, AttributeSet attrs, int defStyleAttr) { disposables = new ViewDisposables(); accessCategory = findViewById(R.id.mainSettingsAccessCategory); + proxyCategory = findViewById(R.id.mainSettingsProxyCategory); roleAndAddressCategory = findViewById(R.id.mainSettingsRoleAndAddressCategory); toolbarView.setTitle(R.string.main_settings_title); @@ -73,6 +71,9 @@ public void onAttachedToWindow() { disposables.add(clicks(accessCategory).subscribe(o -> navigator.execute( Transaction.push(SettingsAccessScreen.create())))); + disposables.add(clicks(proxyCategory).subscribe(o -> + navigator.execute( + Transaction.push(SettingsProxyScreen.create())))); disposables.add(clicks(roleAndAddressCategory).subscribe(o -> navigator.execute( Transaction.push(SettingsRoleAndAddressScreen.create())))); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyScreen.java new file mode 100644 index 000000000..8bbec174c --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyScreen.java @@ -0,0 +1,44 @@ +package ee.ria.DigiDoc.android.main.settings.proxy; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.utils.navigator.conductor.ConductorScreen; + +public final class SettingsProxyScreen extends ConductorScreen { + + public static SettingsProxyScreen create() { + return new SettingsProxyScreen(); + } + + @SuppressWarnings("WeakerAccess") + public SettingsProxyScreen() { + super(R.id.mainSettingsProxyScreen); + } + + @Override + protected View view(Context context) { + return new SettingsProxyView(context); + } + + @Override + protected void onDestroyView(@NonNull View view) { + FragmentActivity activity = (FragmentActivity) getActivity(); + if (activity != null) { + FragmentManager fragmentManager = activity.getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.mainSettingsFragment); + if (fragment != null) { + fragmentManager.beginTransaction() + .remove(fragment) + .commitAllowingStateLoss(); + } + } + super.onDestroyView(view); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyView.java new file mode 100644 index 000000000..0a61cad8a --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/proxy/SettingsProxyView.java @@ -0,0 +1,256 @@ +package ee.ria.DigiDoc.android.main.settings.proxy; + +import static com.jakewharton.rxbinding4.widget.RxRadioGroup.checkedChanges; +import static com.jakewharton.rxbinding4.widget.RxTextView.textChanges; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.main.settings.util.SettingsUtil.getToolbarViewTitle; +import static ee.ria.DigiDoc.common.ProxySetting.MANUAL_PROXY; +import static ee.ria.DigiDoc.common.ProxySetting.NO_PROXY; +import static ee.ria.DigiDoc.common.ProxySetting.SYSTEM_PROXY; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.CheckBox; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toolbar; + +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; +import com.jakewharton.rxbinding4.widget.RxCompoundButton; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.ApplicationApp; +import ee.ria.DigiDoc.android.main.settings.SettingsDataStore; +import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.navigator.Navigator; +import ee.ria.DigiDoc.android.utils.navigator.Transaction; +import ee.ria.DigiDoc.common.ManualProxy; +import ee.ria.DigiDoc.common.ProxySetting; +import timber.log.Timber; + +public final class SettingsProxyView extends CoordinatorLayout { + + private final Toolbar toolbarView; + + private final Navigator navigator; + private final SettingsDataStore settingsDataStore; + + private final ViewDisposables disposables; + + private final RadioGroup proxyGroup; + private final RadioButton noProxy; + private final RadioButton systemProxy; + private final RadioButton manualProxy; + private final CheckBox allowSSLConnections; + + private final TextInputEditText host; + private final TextInputEditText port; + private final TextInputEditText username; + private final TextInputEditText password; + + private final TextInputLayout portLayout; + + private final ManualProxy manualProxySettings; + + public SettingsProxyView(Context context) { + this(context, null); + } + + public SettingsProxyView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SettingsProxyView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + inflate(context, R.layout.main_settings_proxy, this); + toolbarView = findViewById(R.id.toolbar); + TextView toolbarTitleView = getToolbarViewTitle(toolbarView); + navigator = ApplicationApp.component(context).navigator(); + settingsDataStore = ApplicationApp.component(context).settingsDataStore(); + disposables = new ViewDisposables(); + + proxyGroup = findViewById(R.id.mainSettingsProxyGroup); + noProxy = findViewById(R.id.mainSettingsProxyNoProxy); + systemProxy = findViewById(R.id.mainSettingsProxyUseSystem); + manualProxy = findViewById(R.id.mainSettingsProxyManual); + allowSSLConnections = findViewById(R.id.mainSettingsProxyAllowSSL); + + host = findViewById(R.id.mainSettingsProxyHost); + port = findViewById(R.id.mainSettingsProxyPort); + username = findViewById(R.id.mainSettingsProxyUsername); + password = findViewById(R.id.mainSettingsProxyPassword); + + portLayout = findViewById(R.id.mainSettingsProxyPortLayout); + + manualProxySettings = getManualProxySettings(); + + toolbarView.setTitle(R.string.main_settings_proxy_button); + toolbarView.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); + toolbarView.setNavigationContentDescription(R.string.back); + + if (toolbarTitleView != null) { + toolbarTitleView.setContentDescription("\u202F"); + } + + checkActiveProxySetting(settingsDataStore); + checkManualProxySettings(settingsDataStore, manualProxySettings); + checkIsSSLForProxyEnabled(settingsDataStore); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + disposables.attach(); + disposables.add(navigationClicks(toolbarView).subscribe(o -> + navigator.execute(Transaction.pop()))); + disposables.add(textChanges(port) + .map(CharSequence::toString) + .subscribe(this::validatePortNumber)); + disposables.add(checkedChanges(proxyGroup).subscribe(setting -> + setProxySetting(settingsDataStore, setting))); + disposables.add(RxCompoundButton.checkedChanges(allowSSLConnections).subscribe(isEnabled -> + setIsSSLForProxyEnabled(settingsDataStore, isEnabled))); + } + + @Override + public void onDetachedFromWindow() { + if (settingsDataStore != null) { + ProxySetting currentProxySetting = settingsDataStore.getProxySetting(); + if (currentProxySetting.equals(MANUAL_PROXY)) { + manualProxySettings.setHost(host.getEditableText().toString()); + String portNumber = port.getEditableText().toString(); + try { + manualProxySettings.setPort( + portNumber.isEmpty() || !isValidPortNumber(portNumber) ? 80 : + Integer.parseInt(port.getEditableText().toString())); + } catch (NumberFormatException nfe) { + Timber.log(Log.ERROR, nfe, "Unable to get the port number"); + manualProxySettings.setPort(80); + } + manualProxySettings.setUsername(username.getEditableText().toString()); + manualProxySettings.setPassword(password.getEditableText().toString()); + setManualProxySettings(settingsDataStore, manualProxySettings); + } else { + clearProxySettings(settingsDataStore); + } + } + + disposables.detach(); + super.onDetachedFromWindow(); + } + + private void checkActiveProxySetting(SettingsDataStore settingsDataStore) { + if (settingsDataStore != null) { + ProxySetting currentProxySetting = settingsDataStore.getProxySetting(); + switch (currentProxySetting) { + case NO_PROXY -> { + noProxy.setChecked(true); + isManualProxyEnabled(false); + } + case SYSTEM_PROXY -> { + systemProxy.setChecked(true); + isManualProxyEnabled(false); + } + case MANUAL_PROXY -> { + manualProxy.setChecked(true); + isManualProxyEnabled(true); + } + } + } + } + + private void setProxySetting(SettingsDataStore settingsDataStore, int buttonId) { + if (settingsDataStore != null) { + if (noProxy.getId() == buttonId) { + settingsDataStore.setProxySetting(NO_PROXY); + } else if (systemProxy.getId() == buttonId) { + settingsDataStore.setProxySetting(SYSTEM_PROXY); + } else if (manualProxy.getId() == buttonId) { + settingsDataStore.setProxySetting(MANUAL_PROXY); + } + + checkActiveProxySetting(settingsDataStore); + } + } + + private ManualProxy getManualProxySettings() { + return new ManualProxy( + settingsDataStore.getProxyHost(), + settingsDataStore.getProxyPort(), + settingsDataStore.getProxyUsername(), + settingsDataStore.getProxyPassword(navigator.activity()) + ); + } + + private void checkManualProxySettings(SettingsDataStore settingsDataStore, ManualProxy manualProxy) { + if (settingsDataStore != null) { + host.setText(manualProxy.getHost()); + port.setText(String.valueOf(manualProxy.getPort())); + username.setText(manualProxy.getUsername()); + password.setText(manualProxy.getPassword()); + } + } + + private void setManualProxySettings(SettingsDataStore settingsDataStore, ManualProxy manualProxy) { + if (settingsDataStore != null) { + settingsDataStore.setProxyHost(manualProxy.getHost()); + settingsDataStore.setProxyPort(manualProxy.getPort()); + settingsDataStore.setProxyUsername(manualProxy.getUsername()); + settingsDataStore.setProxyPassword(navigator.activity(), manualProxy.getPassword()); + } + } + + private void checkIsSSLForProxyEnabled(SettingsDataStore settingsDataStore) { + if (settingsDataStore != null) { + boolean isSSLForProxyEnabled = settingsDataStore.getIsProxyForSSLEnabled(); + allowSSLConnections.setChecked(isSSLForProxyEnabled); + } + } + + private void setIsSSLForProxyEnabled(SettingsDataStore settingsDataStore, boolean isEnabled) { + if (settingsDataStore != null) { + settingsDataStore.setIsProxyForSSLEnabled(isEnabled); + } + } + + private void clearProxySettings(SettingsDataStore settingsDataStore) { + manualProxySettings.setHost(""); + manualProxySettings.setPort(80); + manualProxySettings.setUsername(""); + manualProxySettings.setPassword(""); + setManualProxySettings(settingsDataStore, manualProxySettings); + } + + private void isManualProxyEnabled(boolean isEnabled) { + host.setEnabled(isEnabled); + port.setEnabled(isEnabled); + username.setEnabled(isEnabled); + password.setEnabled(isEnabled); + allowSSLConnections.setEnabled(isEnabled); + } + + private void validatePortNumber(String portNumber) { + if (!portNumber.isEmpty()) { + if (!isValidPortNumber(portNumber)) { + portLayout.setError("Min 1, max 65535"); + } else { + portLayout.setError(null); + } + } + } + + private static boolean isValidPortNumber(String portNumber) { + try { + int number = Integer.parseInt(portNumber); + return number >= 1 && number <= 65535; + } catch (NumberFormatException e) { + Timber.log(Log.ERROR, e, String.format("Invalid number: %s", portNumber)); + return false; + } + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java index 4f1ecf562..de0a833d9 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java @@ -101,7 +101,9 @@ Observable sign(File containerFile, Observable.create(new MobileIdOnSubscribe(navigator, container, localeService.applicationLocale(), settingsDataStore.getUuid(), mobileIdRequest.personalCode(), - mobileIdRequest.phoneNo(), roleData))) + mobileIdRequest.phoneNo(), settingsDataStore.getIsProxyForSSLEnabled(), + settingsDataStore.getProxySetting(), + settingsDataStore.getManualProxySettings(navigator.activity()), roleData))) .switchMap(response -> { String signature = response.signature(); Button mobileIdCancelButton = navigator.activity().findViewById(R.id.signatureUpdateMobileIdCancelButton); @@ -137,7 +139,10 @@ Observable sign(File containerFile, .flatMapObservable(container -> Observable.create(new SmartIdOnSubscribe(navigator, container, localeService.applicationLocale(), settingsDataStore.getUuid(), - smartIdRequest.personalCode(), smartIdRequest.country(), roleData))) + smartIdRequest.personalCode(), smartIdRequest.country(), + settingsDataStore.getIsProxyForSSLEnabled(), + settingsDataStore.getProxySetting(), + settingsDataStore.getManualProxySettings(navigator.activity()), roleData))) .switchMap(response -> { SessionStatusResponse.ProcessStatus processStatus = response.status(); Button smartIdCancelButton = navigator.activity().findViewById(R.id.signatureUpdateSmartIdCancelButton); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java index b4de58e77..d8387fd6e 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java @@ -7,8 +7,14 @@ import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_CHALLENGE; import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_REQUEST; import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_STATUS; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MANUAL_PROXY_HOST; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MANUAL_PROXY_PASSWORD; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MANUAL_PROXY_PORT; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MANUAL_PROXY_USERNAME; import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MID_BROADCAST_ACTION; import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MID_BROADCAST_TYPE_KEY; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.PROXY_IS_SSL_ENABLED; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.PROXY_SETTING; import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.SERVICE_FAULT; import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.SIGNING_ROLE_DATA; @@ -36,6 +42,8 @@ import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.model.mobileid.MobileIdMessageException; import ee.ria.DigiDoc.android.utils.navigator.Navigator; +import ee.ria.DigiDoc.common.ManualProxy; +import ee.ria.DigiDoc.common.ProxySetting; import ee.ria.DigiDoc.common.RoleData; import ee.ria.DigiDoc.configuration.ConfigurationProvider; import ee.ria.DigiDoc.mobileid.dto.request.MobileCreateSignatureRequest; @@ -56,12 +64,16 @@ public final class MobileIdOnSubscribe implements ObservableOnSubscribe + + +