diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2020-10-08 17:15:39 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-08 17:15:39 +0530 |
commit | b4f6fc502acf69e7f6f63354d07a8c07939f36ba (patch) | |
tree | 8a4131c13b545a68bf2140c47d97fdd3aab57a9d | |
parent | 0d6b7f1842a90bfefbe6f47725871a7d70ba31fb (diff) |
Introduce app-wide HTTPS proxy setting (#1134)
11 files changed, 322 insertions, 7 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b0781f4..6cad066e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,9 @@ android:name=".ui.onboarding.activity.OnboardingActivity" android:configChanges="orientation|screenSize" /> + <activity android:name=".ui.proxy.ProxySelectorActivity" + android:windowSoftInputMode="adjustResize" /> + <activity android:name=".LaunchActivity" android:configChanges="orientation|screenSize" diff --git a/app/src/main/java/com/zeapo/pwdstore/Application.kt b/app/src/main/java/com/zeapo/pwdstore/Application.kt index 91e47793..3f3963d0 100644 --- a/app/src/main/java/com/zeapo/pwdstore/Application.kt +++ b/app/src/main/java/com/zeapo/pwdstore/Application.kt @@ -14,27 +14,31 @@ import com.github.ajalt.timberkt.Timber.DebugTree import com.github.ajalt.timberkt.Timber.plant import com.zeapo.pwdstore.git.sshj.setUpBouncyCastleForSshj import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.ProxyUtils import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs @Suppress("Unused") class Application : android.app.Application(), SharedPreferences.OnSharedPreferenceChangeListener { + private val prefs by lazy { sharedPrefs } + override fun onCreate() { super.onCreate() instance = this if (BuildConfig.ENABLE_DEBUG_FEATURES || - sharedPrefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)) { + prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)) { plant(DebugTree()) } - sharedPrefs.registerOnSharedPreferenceChangeListener(this) + prefs.registerOnSharedPreferenceChangeListener(this) setNightMode() setUpBouncyCastleForSshj() runMigrations(applicationContext) + ProxyUtils.setDefaultProxy() } override fun onTerminate() { - sharedPrefs.unregisterOnSharedPreferenceChangeListener(this) + prefs.unregisterOnSharedPreferenceChangeListener(this) super.onTerminate() } @@ -45,7 +49,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere } private fun setNightMode() { - AppCompatDelegate.setDefaultNightMode(when (sharedPrefs.getString(PreferenceKeys.APP_THEME) + AppCompatDelegate.setDefaultNightMode(when (prefs.getString(PreferenceKeys.APP_THEME) ?: getString(R.string.app_theme_def)) { "light" -> MODE_NIGHT_NO "dark" -> MODE_NIGHT_YES diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 45915213..7f6727f2 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -50,6 +50,7 @@ import com.zeapo.pwdstore.git.sshj.SshKey import com.zeapo.pwdstore.pwgenxkpwd.XkpwdDictionary import com.zeapo.pwdstore.sshkeygen.ShowSshKeyFragment import com.zeapo.pwdstore.sshkeygen.SshKeyGenActivity +import com.zeapo.pwdstore.ui.proxy.ProxySelectorActivity import com.zeapo.pwdstore.utils.BiometricAuthenticator import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys @@ -418,6 +419,11 @@ class UserPreference : AppCompatActivity() { } } + findPreference<Preference>(PreferenceKeys.PROXY_SETTINGS)?.onPreferenceClickListener = ClickListener { + startActivity(Intent(requireContext(), ProxySelectorActivity::class.java)) + true + } + val prefCustomXkpwdDictionary = findPreference<Preference>(PreferenceKeys.PREF_KEY_CUSTOM_DICT) prefCustomXkpwdDictionary?.onPreferenceClickListener = ClickListener { prefsActivity.storeCustomDictionaryPath() diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt index f928590d..27ceb5cb 100644 --- a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt +++ b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt @@ -11,6 +11,7 @@ import com.zeapo.pwdstore.Application import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PreferenceKeys import com.zeapo.pwdstore.utils.getEncryptedGitPrefs +import com.zeapo.pwdstore.utils.getEncryptedProxyPrefs import com.zeapo.pwdstore.utils.getString import com.zeapo.pwdstore.utils.sharedPrefs import java.io.File @@ -54,6 +55,7 @@ object GitSettings { private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.sharedPrefs } private val encryptedSettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedGitPrefs() } + private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedProxyPrefs() } var authMode get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH)) @@ -108,6 +110,38 @@ object GitSettings { } } + var proxyHost + get() = proxySettings.getString(PreferenceKeys.PROXY_HOST) + set(value) { + proxySettings.edit { + putString(PreferenceKeys.PROXY_HOST, value) + } + } + + var proxyPort + get() = proxySettings.getInt(PreferenceKeys.PROXY_PORT, -1) + set(value) { + proxySettings.edit { + putInt(PreferenceKeys.PROXY_PORT, value) + } + } + + var proxyUsername + get() = settings.getString(PreferenceKeys.PROXY_USERNAME) + set(value) { + proxySettings.edit { + putString(PreferenceKeys.PROXY_USERNAME, value) + } + } + + var proxyPassword + get() = proxySettings.getString(PreferenceKeys.PROXY_PASSWORD) + set(value) { + proxySettings.edit { + putString(PreferenceKeys.PROXY_PASSWORD, value) + } + } + sealed class UpdateConnectionSettingsResult { class MissingUsername(val newProtocol: Protocol) : UpdateConnectionSettingsResult() class AuthModeMismatch(val newProtocol: Protocol, val validModes: List<AuthMode>) : UpdateConnectionSettingsResult() diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/proxy/ProxySelectorActivity.kt b/app/src/main/java/com/zeapo/pwdstore/ui/proxy/ProxySelectorActivity.kt new file mode 100644 index 00000000..29c4d53a --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/ui/proxy/ProxySelectorActivity.kt @@ -0,0 +1,75 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.ui.proxy + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Patterns +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.edit +import androidx.core.os.postDelayed +import androidx.core.widget.doOnTextChanged +import com.zeapo.pwdstore.R +import com.zeapo.pwdstore.databinding.ActivityProxySelectorBinding +import com.zeapo.pwdstore.git.config.GitSettings +import com.zeapo.pwdstore.utils.PreferenceKeys +import com.zeapo.pwdstore.utils.ProxyUtils +import com.zeapo.pwdstore.utils.getEncryptedProxyPrefs +import com.zeapo.pwdstore.utils.getString +import com.zeapo.pwdstore.utils.viewBinding + +private val IP_ADDRESS_REGEX = Patterns.IP_ADDRESS.toRegex() +private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex() + +class ProxySelectorActivity : AppCompatActivity() { + + private val binding by viewBinding(ActivityProxySelectorBinding::inflate) + private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) { applicationContext.getEncryptedProxyPrefs() } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + with(binding) { + proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST)) + proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME)) + proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let { + proxyPort.setText("$it") + } + proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD)) + save.setOnClickListener { saveSettings() } + proxyHost.doOnTextChanged { text, _, _, _ -> + if (text != null) { + proxyHost.error = if (text.matches(IP_ADDRESS_REGEX) || text.matches(WEB_ADDRESS_REGEX)) { + null + } else { + getString(R.string.invalid_proxy_url) + } + } + } + } + + } + + private fun saveSettings() { + proxyPrefs.edit { + binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { + GitSettings.proxyHost = it + } + binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let { + GitSettings.proxyUsername = it + } + binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let { + GitSettings.proxyPort = it.toInt() + } + binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let { + GitSettings.proxyPassword = it + } + } + ProxyUtils.setDefaultProxy() + Handler(Looper.getMainLooper()).postDelayed(500) { finish() } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt index 3ee1820d..408e9d5e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/AndroidExtensions.kt @@ -72,6 +72,12 @@ val Context.clipboard fun Context.getEncryptedGitPrefs() = getEncryptedPrefs("git_operation") /** + * Wrapper for [getEncryptedPrefs] to get the encrypted preference set for the HTTP + * proxy. + */ +fun Context.getEncryptedProxyPrefs() = getEncryptedPrefs("http_proxy") + +/** * Get an instance of [EncryptedSharedPreferences] with the given [fileName] */ private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences { diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt index 1b2c7abb..5ec40639 100644 --- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt +++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt @@ -80,4 +80,10 @@ object PreferenceKeys { @Deprecated("To be used only in Migrations.kt") const val USE_GENERATED_KEY = "use_generated_key" + + const val PROXY_SETTINGS = "proxy_settings" + const val PROXY_HOST = "proxy_host" + const val PROXY_PORT = "proxy_port" + const val PROXY_USERNAME = "proxy_username" + const val PROXY_PASSWORD = "proxy_password" } diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/ProxyUtils.kt b/app/src/main/java/com/zeapo/pwdstore/utils/ProxyUtils.kt new file mode 100644 index 00000000..f6401a1d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/ProxyUtils.kt @@ -0,0 +1,66 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package com.zeapo.pwdstore.utils + +import com.zeapo.pwdstore.git.config.GitSettings +import java.io.IOException +import java.net.Authenticator +import java.net.InetSocketAddress +import java.net.PasswordAuthentication +import java.net.Proxy +import java.net.ProxySelector +import java.net.SocketAddress +import java.net.URI + +/** + * Utility class for [Proxy] handling. + */ +object ProxyUtils { + + private const val HTTP_PROXY_USER_PROPERTY = "http.proxyUser" + private const val HTTP_PROXY_PASSWORD_PROPERTY = "http.proxyPassword" + + /** + * Set the default [Proxy] and [Authenticator] for the app based on user provided settings. + */ + fun setDefaultProxy() { + ProxySelector.setDefault(object : ProxySelector() { + override fun select(uri: URI?): MutableList<Proxy> { + val host = GitSettings.proxyHost + val port = GitSettings.proxyPort + return if (host == null || port == -1) { + mutableListOf() + } else { + mutableListOf(Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(host, port))) + } + } + + override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) { + if (uri == null || sa == null || ioe == null) { + throw IllegalArgumentException("Arguments can't be null.") + } + } + }) + val user = GitSettings.proxyUsername ?: "" + val password = GitSettings.proxyPassword ?: "" + if (user.isEmpty() || password.isEmpty()) { + System.clearProperty(HTTP_PROXY_USER_PROPERTY) + System.clearProperty(HTTP_PROXY_PASSWORD_PROPERTY) + } else { + System.setProperty(HTTP_PROXY_USER_PROPERTY, user) + System.setProperty(HTTP_PROXY_PASSWORD_PROPERTY, password) + } + Authenticator.setDefault(object : Authenticator() { + override fun getPasswordAuthentication(): PasswordAuthentication? { + return if (requestorType == RequestorType.PROXY) { + PasswordAuthentication(user, password.toCharArray()) + } else { + null + } + } + }) + } +} diff --git a/app/src/main/res/layout/activity_proxy_selector.xml b/app/src/main/res/layout/activity_proxy_selector.xml new file mode 100644 index 00000000..e732dfe0 --- /dev/null +++ b/app/src/main/res/layout/activity_proxy_selector.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + ~ SPDX-License-Identifier: GPL-3.0-only + --> + +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_host_input_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:hint="@string/proxy_hostname" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout_editor_absoluteY="64dp"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_host" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textUri" + android:nextFocusForward="@id/proxy_user" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_user_input_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/normal_margin" + android:hint="@string/username" + app:layout_constraintEnd_toStartOf="@id/proxy_port_input_layout" + app:layout_constraintHorizontal_weight="0.65" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/proxy_host_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_user" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textWebEmailAddress" + android:nextFocusForward="@id/proxy_port" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_port_input_layout" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/normal_margin" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:hint="@string/port" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_weight="0.35" + app:layout_constraintStart_toEndOf="@id/proxy_user_input_layout" + app:layout_constraintTop_toBottomOf="@id/proxy_host_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_port" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="number" + android:nextFocusForward="@id/proxy_password" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/proxy_password_input_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/activity_horizontal_margin" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:hint="@string/password" + app:endIconMode="password_toggle" + app:layout_constraintStart_toStartOf="@id/proxy_user_input_layout" + app:layout_constraintTop_toBottomOf="@id/proxy_user_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/proxy_password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textPassword" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.button.MaterialButton + android:id="@+id/save" + style="@style/AppTheme.OutlinedButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/normal_margin" + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:text="@string/crypto_save" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/proxy_password_input_layout" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ed3ee52..cc9f1f24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,10 +100,10 @@ <!-- DECRYPT Layout --> <string name="action_search">Search</string> - <string name="password">Password:</string> - <string name="otp">OTP:</string> + <string name="password">Password</string> + <string name="otp">OTP</string> <string name="extra_content">Extra content:</string> - <string name="username">Username:</string> + <string name="username">Username</string> <string name="edit_password">Edit password</string> <string name="copy_password">Copy password</string> <string name="share_as_plaintext">Share as plaintext</string> @@ -408,4 +408,10 @@ <string name="ssh_scheme_needed_title">Potentially incorrect URL</string> <string name="ssh_scheme_needed_message">It appears that your URL contains a custom port, but does not specify the ssh:// scheme.\nThis can cause the port to be considered a part of your path. Press OK here to fix the URL.</string> + <!-- Proxy configuration activity --> + <string name="proxy_hostname">Proxy hostname</string> + <string name="port">Port</string> + <string name="pref_proxy_settings">HTTP(S) proxy settings</string> + <string name="invalid_proxy_url">Invalid URL</string> + </resources> diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index 1d82df18..58a173c5 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -49,6 +49,9 @@ app:key="git_server_info" app:title="@string/pref_edit_server_info" /> <Preference + app:key="proxy_settings" + app:title="@string/pref_proxy_settings" /> + <Preference app:key="git_config" app:title="@string/pref_edit_git_config" /> <Preference |