Merge pull request #344 from cyb3rko/client-certificate-auth
Client certificate authentication (mTLS)
This commit is contained in:
@@ -4,9 +4,13 @@ import android.app.Application
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.github.gotify.api.CertUtils
|
||||||
import com.github.gotify.log.LoggerHelper
|
import com.github.gotify.log.LoggerHelper
|
||||||
import com.github.gotify.log.UncaughtExceptionHandler
|
import com.github.gotify.log.UncaughtExceptionHandler
|
||||||
import com.github.gotify.settings.ThemeHelper
|
import com.github.gotify.settings.ThemeHelper
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
|
|
||||||
class GotifyApplication : Application() {
|
class GotifyApplication : Application() {
|
||||||
@@ -26,6 +30,22 @@ class GotifyApplication : Application() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val settings = Settings(this)
|
||||||
|
if (settings.legacyCert != null) {
|
||||||
|
Logger.info("Migrating legacy CA cert to new location")
|
||||||
|
try {
|
||||||
|
val legacyCert = settings.legacyCert
|
||||||
|
settings.legacyCert = null
|
||||||
|
val caCertFile = File(settings.filesDir, CertUtils.CA_CERT_NAME)
|
||||||
|
FileOutputStream(caCertFile).use {
|
||||||
|
it.write(legacyCert?.encodeToByteArray())
|
||||||
|
}
|
||||||
|
settings.caCertPath = caCertFile.absolutePath
|
||||||
|
Logger.info("Migration of legacy CA cert succeeded")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Logger.error(e, "Migration of legacy CA cert failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
package com.github.gotify
|
package com.github.gotify
|
||||||
|
|
||||||
internal class SSLSettings(val validateSSL: Boolean, val cert: String?)
|
internal class SSLSettings(
|
||||||
|
val validateSSL: Boolean,
|
||||||
|
val caCertPath: String?,
|
||||||
|
val clientCertPath: String?,
|
||||||
|
val clientCertPassword: String?
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.github.gotify.client.model.User
|
|||||||
|
|
||||||
internal class Settings(context: Context) {
|
internal class Settings(context: Context) {
|
||||||
private val sharedPreferences: SharedPreferences
|
private val sharedPreferences: SharedPreferences
|
||||||
|
val filesDir: String
|
||||||
var url: String
|
var url: String
|
||||||
get() = sharedPreferences.getString("url", "")!!
|
get() = sharedPreferences.getString("url", "")!!
|
||||||
set(value) = sharedPreferences.edit().putString("url", value).apply()
|
set(value) = sharedPreferences.edit().putString("url", value).apply()
|
||||||
@@ -26,15 +27,25 @@ internal class Settings(context: Context) {
|
|||||||
var serverVersion: String
|
var serverVersion: String
|
||||||
get() = sharedPreferences.getString("version", "UNKNOWN")!!
|
get() = sharedPreferences.getString("version", "UNKNOWN")!!
|
||||||
set(value) = sharedPreferences.edit().putString("version", value).apply()
|
set(value) = sharedPreferences.edit().putString("version", value).apply()
|
||||||
var cert: String?
|
var legacyCert: String?
|
||||||
get() = sharedPreferences.getString("cert", null)
|
get() = sharedPreferences.getString("cert", null)
|
||||||
set(value) = sharedPreferences.edit().putString("cert", value).apply()
|
set(value) = sharedPreferences.edit().putString("cert", value).commit().toUnit()
|
||||||
|
var caCertPath: String?
|
||||||
|
get() = sharedPreferences.getString("caCertPath", null)
|
||||||
|
set(value) = sharedPreferences.edit().putString("caCertPath", value).commit().toUnit()
|
||||||
var validateSSL: Boolean
|
var validateSSL: Boolean
|
||||||
get() = sharedPreferences.getBoolean("validateSSL", true)
|
get() = sharedPreferences.getBoolean("validateSSL", true)
|
||||||
set(value) = sharedPreferences.edit().putBoolean("validateSSL", value).apply()
|
set(value) = sharedPreferences.edit().putBoolean("validateSSL", value).apply()
|
||||||
|
var clientCertPath: String?
|
||||||
|
get() = sharedPreferences.getString("clientCertPath", null)
|
||||||
|
set(value) = sharedPreferences.edit().putString("clientCertPath", value).apply()
|
||||||
|
var clientCertPassword: String?
|
||||||
|
get() = sharedPreferences.getString("clientCertPass", null)
|
||||||
|
set(value) = sharedPreferences.edit().putString("clientCertPass", value).apply()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE)
|
sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE)
|
||||||
|
filesDir = context.filesDir.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tokenExists(): Boolean = !token.isNullOrEmpty()
|
fun tokenExists(): Boolean = !token.isNullOrEmpty()
|
||||||
@@ -43,7 +54,10 @@ internal class Settings(context: Context) {
|
|||||||
url = ""
|
url = ""
|
||||||
token = null
|
token = null
|
||||||
validateSSL = true
|
validateSSL = true
|
||||||
cert = null
|
legacyCert = null
|
||||||
|
caCertPath = null
|
||||||
|
clientCertPath = null
|
||||||
|
clientCertPassword = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUser(name: String?, admin: Boolean) {
|
fun setUser(name: String?, admin: Boolean) {
|
||||||
@@ -51,6 +65,14 @@ internal class Settings(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sslSettings(): SSLSettings {
|
fun sslSettings(): SSLSettings {
|
||||||
return SSLSettings(validateSSL, cert)
|
return SSLSettings(
|
||||||
|
validateSSL,
|
||||||
|
caCertPath,
|
||||||
|
clientCertPath,
|
||||||
|
clientCertPassword
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UnusedReceiverParameter")
|
||||||
|
private fun Any?.toUnit() = Unit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ import coil.target.Target
|
|||||||
import com.github.gotify.client.JSON
|
import com.github.gotify.client.JSON
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
@@ -24,7 +20,6 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okio.Buffer
|
|
||||||
import org.threeten.bp.OffsetDateTime
|
import org.threeten.bp.OffsetDateTime
|
||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
|
|
||||||
@@ -80,25 +75,6 @@ internal object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readFileFromStream(inputStream: InputStream): String {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
var currentLine: String?
|
|
||||||
try {
|
|
||||||
BufferedReader(InputStreamReader(inputStream)).use { reader ->
|
|
||||||
while (reader.readLine().also { currentLine = it } != null) {
|
|
||||||
sb.append(currentLine).append("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw IllegalArgumentException("failed to read input")
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stringToInputStream(str: String?): InputStream? {
|
|
||||||
return if (str == null) null else Buffer().writeUtf8(str).inputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AppCompatActivity.launchCoroutine(
|
fun AppCompatActivity.launchCoroutine(
|
||||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
action: suspend (coroutineScope: CoroutineScope) -> Unit
|
action: suspend (coroutineScope: CoroutineScope) -> Unit
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ package com.github.gotify.api
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import com.github.gotify.SSLSettings
|
import com.github.gotify.SSLSettings
|
||||||
import com.github.gotify.Utils
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStream
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.KeyManager
|
||||||
|
import javax.net.ssl.KeyManagerFactory
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.TrustManagerFactory
|
import javax.net.ssl.TrustManagerFactory
|
||||||
@@ -18,6 +21,9 @@ import okhttp3.OkHttpClient
|
|||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
|
|
||||||
internal object CertUtils {
|
internal object CertUtils {
|
||||||
|
const val CA_CERT_NAME = "ca-cert.crt"
|
||||||
|
const val CLIENT_CERT_NAME = "client-cert.p12"
|
||||||
|
|
||||||
@SuppressLint("CustomX509TrustManager")
|
@SuppressLint("CustomX509TrustManager")
|
||||||
private val trustAll = object : X509TrustManager {
|
private val trustAll = object : X509TrustManager {
|
||||||
@SuppressLint("TrustAllX509TrustManager")
|
@SuppressLint("TrustAllX509TrustManager")
|
||||||
@@ -31,10 +37,10 @@ internal object CertUtils {
|
|||||||
override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
|
override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseCertificate(cert: String): Certificate {
|
fun parseCertificate(inputStream: InputStream): Certificate {
|
||||||
try {
|
try {
|
||||||
val certificateFactory = CertificateFactory.getInstance("X509")
|
val certificateFactory = CertificateFactory.getInstance("X509")
|
||||||
return certificateFactory.generateCertificate(Utils.stringToInputStream(cert))
|
return certificateFactory.generateCertificate(inputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalArgumentException("certificate is invalid")
|
throw IllegalArgumentException("certificate is invalid")
|
||||||
}
|
}
|
||||||
@@ -43,25 +49,35 @@ internal object CertUtils {
|
|||||||
fun applySslSettings(builder: OkHttpClient.Builder, settings: SSLSettings) {
|
fun applySslSettings(builder: OkHttpClient.Builder, settings: SSLSettings) {
|
||||||
// Modified from ApiClient.applySslSettings in the client package.
|
// Modified from ApiClient.applySslSettings in the client package.
|
||||||
try {
|
try {
|
||||||
if (!settings.validateSSL) {
|
val trustManagers = mutableSetOf<TrustManager>()
|
||||||
val context = SSLContext.getInstance("TLS")
|
val keyManagers = mutableSetOf<KeyManager>()
|
||||||
context.init(arrayOf(), arrayOf<TrustManager>(trustAll), SecureRandom())
|
if (settings.validateSSL) {
|
||||||
builder.sslSocketFactory(context.socketFactory, trustAll)
|
// Custom SSL validation
|
||||||
|
settings.caCertPath?.let { trustManagers.addAll(certToTrustManager(it)) }
|
||||||
|
} else {
|
||||||
|
// Disable SSL validation
|
||||||
|
trustManagers.add(trustAll)
|
||||||
builder.hostnameVerifier { _, _ -> true }
|
builder.hostnameVerifier { _, _ -> true }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
val cert = settings.cert
|
settings.clientCertPath?.let {
|
||||||
if (cert != null) {
|
keyManagers.addAll(certToKeyManager(it, settings.clientCertPassword))
|
||||||
val trustManagers = certToTrustManager(cert)
|
}
|
||||||
if (trustManagers.isNotEmpty()) {
|
if (trustManagers.isNotEmpty() || keyManagers.isNotEmpty()) {
|
||||||
|
if (trustManagers.isEmpty()) {
|
||||||
|
// Fall back to system trust managers
|
||||||
|
trustManagers.addAll(defaultSystemTrustManager())
|
||||||
|
}
|
||||||
val context = SSLContext.getInstance("TLS")
|
val context = SSLContext.getInstance("TLS")
|
||||||
context.init(arrayOf(), trustManagers, SecureRandom())
|
context.init(
|
||||||
|
keyManagers.toTypedArray(),
|
||||||
|
trustManagers.toTypedArray(),
|
||||||
|
SecureRandom()
|
||||||
|
)
|
||||||
builder.sslSocketFactory(
|
builder.sslSocketFactory(
|
||||||
context.socketFactory,
|
context.socketFactory,
|
||||||
trustManagers[0] as X509TrustManager
|
trustManagers.elementAt(0) as X509TrustManager
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// We shouldn't have issues since the cert is verified on login.
|
// We shouldn't have issues since the cert is verified on login.
|
||||||
Logger.error(e, "Failed to apply SSL settings")
|
Logger.error(e, "Failed to apply SSL settings")
|
||||||
@@ -69,12 +85,14 @@ internal object CertUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(GeneralSecurityException::class)
|
@Throws(GeneralSecurityException::class)
|
||||||
private fun certToTrustManager(cert: String): Array<TrustManager> {
|
private fun certToTrustManager(certPath: String): Array<TrustManager> {
|
||||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||||
val certificates = certificateFactory.generateCertificates(Utils.stringToInputStream(cert))
|
val certificates = FileInputStream(File(certPath)).use(
|
||||||
|
certificateFactory::generateCertificates
|
||||||
|
)
|
||||||
require(certificates.isNotEmpty()) { "expected non-empty set of trusted certificates" }
|
require(certificates.isNotEmpty()) { "expected non-empty set of trusted certificates" }
|
||||||
|
|
||||||
val caKeyStore = newEmptyKeyStore()
|
val caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null) }
|
||||||
certificates.forEachIndexed { index, certificate ->
|
certificates.forEachIndexed { index, certificate ->
|
||||||
val certificateAlias = "ca$index"
|
val certificateAlias = "ca$index"
|
||||||
caKeyStore.setCertificateEntry(certificateAlias, certificate)
|
caKeyStore.setCertificateEntry(certificateAlias, certificate)
|
||||||
@@ -86,13 +104,24 @@ internal object CertUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(GeneralSecurityException::class)
|
@Throws(GeneralSecurityException::class)
|
||||||
private fun newEmptyKeyStore(): KeyStore {
|
private fun certToKeyManager(certPath: String, certPassword: String?): Array<KeyManager> {
|
||||||
return try {
|
require(certPassword != null) { "empty client certificate password" }
|
||||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
|
||||||
keyStore.load(null, null)
|
val keyStore = KeyStore.getInstance("PKCS12")
|
||||||
keyStore
|
FileInputStream(File(certPath)).use {
|
||||||
} catch (e: IOException) {
|
keyStore.load(it, certPassword.toCharArray())
|
||||||
throw AssertionError(e)
|
}
|
||||||
}
|
val keyManagerFactory =
|
||||||
|
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||||
|
keyManagerFactory.init(keyStore, certPassword.toCharArray())
|
||||||
|
return keyManagerFactory.keyManagers
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultSystemTrustManager(): Array<TrustManager> {
|
||||||
|
val trustManagerFactory = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm()
|
||||||
|
)
|
||||||
|
trustManagerFactory.init(null as KeyStore?)
|
||||||
|
return trustManagerFactory.trustManagers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,55 +9,55 @@ import com.github.gotify.client.auth.ApiKeyAuth
|
|||||||
import com.github.gotify.client.auth.HttpBasicAuth
|
import com.github.gotify.client.auth.HttpBasicAuth
|
||||||
|
|
||||||
internal object ClientFactory {
|
internal object ClientFactory {
|
||||||
private fun unauthorized(baseUrl: String, sslSettings: SSLSettings): ApiClient {
|
private fun unauthorized(
|
||||||
return defaultClient(arrayOf(), "$baseUrl/", sslSettings)
|
settings: Settings,
|
||||||
|
sslSettings: SSLSettings,
|
||||||
|
baseUrl: String
|
||||||
|
): ApiClient {
|
||||||
|
return defaultClient(arrayOf(), settings, sslSettings, baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun basicAuth(
|
fun basicAuth(
|
||||||
baseUrl: String,
|
settings: Settings,
|
||||||
sslSettings: SSLSettings,
|
sslSettings: SSLSettings,
|
||||||
username: String,
|
username: String,
|
||||||
password: String
|
password: String
|
||||||
): ApiClient {
|
): ApiClient {
|
||||||
val client = defaultClient(
|
val client = defaultClient(arrayOf("basicAuth"), settings, sslSettings)
|
||||||
arrayOf("basicAuth"),
|
|
||||||
"$baseUrl/",
|
|
||||||
sslSettings
|
|
||||||
)
|
|
||||||
val auth = client.apiAuthorizations["basicAuth"] as HttpBasicAuth
|
val auth = client.apiAuthorizations["basicAuth"] as HttpBasicAuth
|
||||||
auth.username = username
|
auth.username = username
|
||||||
auth.password = password
|
auth.password = password
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clientToken(baseUrl: String, sslSettings: SSLSettings, token: String?): ApiClient {
|
fun clientToken(settings: Settings, token: String? = settings.token): ApiClient {
|
||||||
val client = defaultClient(
|
val client = defaultClient(arrayOf("clientTokenHeader"), settings)
|
||||||
arrayOf("clientTokenHeader"),
|
|
||||||
"$baseUrl/",
|
|
||||||
sslSettings
|
|
||||||
)
|
|
||||||
val tokenAuth = client.apiAuthorizations["clientTokenHeader"] as ApiKeyAuth
|
val tokenAuth = client.apiAuthorizations["clientTokenHeader"] as ApiKeyAuth
|
||||||
tokenAuth.apiKey = token
|
tokenAuth.apiKey = token
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
fun versionApi(baseUrl: String, sslSettings: SSLSettings): VersionApi {
|
fun versionApi(
|
||||||
return unauthorized(baseUrl, sslSettings).createService(VersionApi::class.java)
|
settings: Settings,
|
||||||
|
sslSettings: SSLSettings = settings.sslSettings(),
|
||||||
|
baseUrl: String = settings.url
|
||||||
|
): VersionApi {
|
||||||
|
return unauthorized(settings, sslSettings, baseUrl).createService(VersionApi::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun userApiWithToken(settings: Settings): UserApi {
|
fun userApiWithToken(settings: Settings): UserApi {
|
||||||
return clientToken(settings.url, settings.sslSettings(), settings.token)
|
return clientToken(settings).createService(UserApi::class.java)
|
||||||
.createService(UserApi::class.java)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultClient(
|
private fun defaultClient(
|
||||||
authentications: Array<String>,
|
authentications: Array<String>,
|
||||||
baseUrl: String,
|
settings: Settings,
|
||||||
sslSettings: SSLSettings
|
sslSettings: SSLSettings = settings.sslSettings(),
|
||||||
|
baseUrl: String = settings.url
|
||||||
): ApiClient {
|
): ApiClient {
|
||||||
val client = ApiClient(authentications)
|
val client = ApiClient(authentications)
|
||||||
CertUtils.applySslSettings(client.okBuilder, sslSettings)
|
CertUtils.applySslSettings(client.okBuilder, sslSettings)
|
||||||
client.adapterBuilder.baseUrl(baseUrl)
|
client.adapterBuilder.baseUrl("$baseUrl/")
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ internal class InitializationActivity : AppCompatActivity() {
|
|||||||
callback: SuccessCallback<VersionInfo>,
|
callback: SuccessCallback<VersionInfo>,
|
||||||
errorCallback: Callback.ErrorCallback
|
errorCallback: Callback.ErrorCallback
|
||||||
) {
|
) {
|
||||||
ClientFactory.versionApi(settings.url, settings.sslSettings())
|
ClientFactory.versionApi(settings)
|
||||||
.version
|
.version
|
||||||
.enqueue(Callback.callInUI(this, callback, errorCallback))
|
.enqueue(Callback.callInUI(this, callback, errorCallback))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.github.gotify.login
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
import com.github.gotify.R
|
import com.github.gotify.R
|
||||||
import com.github.gotify.databinding.AdvancedSettingsDialogBinding
|
import com.github.gotify.databinding.AdvancedSettingsDialogBinding
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@@ -15,6 +16,9 @@ internal class AdvancedDialog(
|
|||||||
private var onCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
private var onCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
private lateinit var onClickSelectCaCertificate: Runnable
|
private lateinit var onClickSelectCaCertificate: Runnable
|
||||||
private lateinit var onClickRemoveCaCertificate: Runnable
|
private lateinit var onClickRemoveCaCertificate: Runnable
|
||||||
|
private lateinit var onClickSelectClientCertificate: Runnable
|
||||||
|
private lateinit var onClickRemoveClientCertificate: Runnable
|
||||||
|
private lateinit var onClose: (password: String) -> Unit
|
||||||
|
|
||||||
fun onDisableSSLChanged(
|
fun onDisableSSLChanged(
|
||||||
onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?
|
onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?
|
||||||
@@ -33,35 +37,101 @@ internal class AdvancedDialog(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show(disableSSL: Boolean, selectedCertificate: String?): AdvancedDialog {
|
fun onClickSelectClientCertificate(onClickSelectClientCertificate: Runnable): AdvancedDialog {
|
||||||
|
this.onClickSelectClientCertificate = onClickSelectClientCertificate
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClickRemoveClientCertificate(onClickRemoveClientCertificate: Runnable): AdvancedDialog {
|
||||||
|
this.onClickRemoveClientCertificate = onClickRemoveClientCertificate
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClose(onClose: (password: String) -> Unit): AdvancedDialog {
|
||||||
|
this.onClose = onClose
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(
|
||||||
|
disableSSL: Boolean,
|
||||||
|
caCertPath: String? = null,
|
||||||
|
caCertCN: String?,
|
||||||
|
clientCertPath: String? = null,
|
||||||
|
clientCertPassword: String?
|
||||||
|
): AdvancedDialog {
|
||||||
binding = AdvancedSettingsDialogBinding.inflate(layoutInflater)
|
binding = AdvancedSettingsDialogBinding.inflate(layoutInflater)
|
||||||
binding.disableSSL.isChecked = disableSSL
|
binding.disableSSL.isChecked = disableSSL
|
||||||
binding.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener)
|
binding.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||||
if (selectedCertificate == null) {
|
if (!clientCertPassword.isNullOrEmpty()) {
|
||||||
showSelectCACertificate()
|
binding.clientCertPasswordEdittext.setText(clientCertPassword)
|
||||||
|
}
|
||||||
|
binding.clientCertPasswordEdittext.doOnTextChanged { _, _, _, _ ->
|
||||||
|
if (binding.selectedClientCert.text.toString() ==
|
||||||
|
context.getString(R.string.certificate_found)
|
||||||
|
) {
|
||||||
|
showPasswordMissing(binding.clientCertPasswordEdittext.text.toString().isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (caCertPath == null) {
|
||||||
|
showSelectCaCertificate()
|
||||||
} else {
|
} else {
|
||||||
showRemoveCACertificate(selectedCertificate)
|
showRemoveCaCertificate(caCertCN!!)
|
||||||
|
}
|
||||||
|
if (clientCertPath == null) {
|
||||||
|
showSelectClientCertificate()
|
||||||
|
} else {
|
||||||
|
showRemoveClientCertificate()
|
||||||
}
|
}
|
||||||
MaterialAlertDialogBuilder(context)
|
MaterialAlertDialogBuilder(context)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.setTitle(R.string.advanced_settings)
|
.setTitle(R.string.advanced_settings)
|
||||||
.setPositiveButton(context.getString(R.string.done), null)
|
.setPositiveButton(context.getString(R.string.done), null)
|
||||||
|
.setOnDismissListener {
|
||||||
|
onClose(binding.clientCertPasswordEdittext.text.toString())
|
||||||
|
}
|
||||||
.show()
|
.show()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showSelectCACertificate() {
|
private fun showSelectCaCertificate() {
|
||||||
binding.toggleCaCert.setText(R.string.select_ca_certificate)
|
binding.toggleCaCert.setText(R.string.select_ca_certificate)
|
||||||
binding.toggleCaCert.setOnClickListener { onClickSelectCaCertificate.run() }
|
binding.toggleCaCert.setOnClickListener { onClickSelectCaCertificate.run() }
|
||||||
binding.selecetedCaCert.setText(R.string.no_certificate_selected)
|
binding.selectedCaCert.setText(R.string.no_certificate_selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showRemoveCACertificate(certificate: String) {
|
fun showRemoveCaCertificate(certificateCN: String) {
|
||||||
binding.toggleCaCert.setText(R.string.remove_ca_certificate)
|
binding.toggleCaCert.setText(R.string.remove_ca_certificate)
|
||||||
binding.toggleCaCert.setOnClickListener {
|
binding.toggleCaCert.setOnClickListener {
|
||||||
showSelectCACertificate()
|
showSelectCaCertificate()
|
||||||
onClickRemoveCaCertificate.run()
|
onClickRemoveCaCertificate.run()
|
||||||
}
|
}
|
||||||
binding.selecetedCaCert.text = certificate
|
binding.selectedCaCert.text = certificateCN
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSelectClientCertificate() {
|
||||||
|
binding.toggleClientCert.setText(R.string.select_client_certificate)
|
||||||
|
binding.toggleClientCert.setOnClickListener { onClickSelectClientCertificate.run() }
|
||||||
|
binding.selectedClientCert.setText(R.string.no_certificate_selected)
|
||||||
|
showPasswordMissing(false)
|
||||||
|
binding.clientCertPasswordEdittext.text = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRemoveClientCertificate() {
|
||||||
|
binding.toggleClientCert.setText(R.string.remove_client_certificate)
|
||||||
|
binding.toggleClientCert.setOnClickListener {
|
||||||
|
showSelectClientCertificate()
|
||||||
|
onClickRemoveClientCertificate.run()
|
||||||
|
}
|
||||||
|
binding.selectedClientCert.setText(R.string.certificate_found)
|
||||||
|
showPasswordMissing(binding.clientCertPasswordEdittext.text.toString().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPasswordMissing(toggled: Boolean) {
|
||||||
|
val error = if (toggled) {
|
||||||
|
context.getString(R.string.client_cert_password_missing)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
binding.clientCertPassword.error = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import android.os.Bundle
|
|||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.github.gotify.R
|
import com.github.gotify.R
|
||||||
import com.github.gotify.SSLSettings
|
import com.github.gotify.SSLSettings
|
||||||
@@ -31,6 +33,10 @@ import com.github.gotify.log.LogsActivity
|
|||||||
import com.github.gotify.log.UncaughtExceptionHandler
|
import com.github.gotify.log.UncaughtExceptionHandler
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import org.tinylog.kotlin.Logger
|
import org.tinylog.kotlin.Logger
|
||||||
@@ -40,10 +46,13 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
private lateinit var settings: Settings
|
private lateinit var settings: Settings
|
||||||
|
|
||||||
private var disableSslValidation = false
|
private var disableSslValidation = false
|
||||||
private var caCertContents: String? = null
|
private var caCertCN: String? = null
|
||||||
|
private var caCertPath: String? = null
|
||||||
|
private var clientCertPath: String? = null
|
||||||
|
private var clientCertPassword: String? = null
|
||||||
private lateinit var advancedDialog: AdvancedDialog
|
private lateinit var advancedDialog: AdvancedDialog
|
||||||
|
|
||||||
private val certificateDialogResultLauncher =
|
private val caDialogResultLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
try {
|
try {
|
||||||
require(result.resultCode == RESULT_OK) { "result was ${result.resultCode}" }
|
require(result.resultCode == RESULT_OK) { "result was ${result.resultCode}" }
|
||||||
@@ -52,18 +61,38 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
val uri = result.data!!.data ?: throw IllegalArgumentException("file path was null")
|
val uri = result.data!!.data ?: throw IllegalArgumentException("file path was null")
|
||||||
val fileStream = contentResolver.openInputStream(uri)
|
val fileStream = contentResolver.openInputStream(uri)
|
||||||
?: throw IllegalArgumentException("file path was invalid")
|
?: throw IllegalArgumentException("file path was invalid")
|
||||||
|
val destinationFile = File(filesDir, CertUtils.CA_CERT_NAME)
|
||||||
|
copyStreamToFile(fileStream, destinationFile)
|
||||||
|
|
||||||
val content = Utils.readFileFromStream(fileStream)
|
// temporarily store it (don't store to settings until they decide to login)
|
||||||
val name = getNameOfCertContent(content)
|
caCertCN = getNameOfCertContent(destinationFile) ?: "unknown"
|
||||||
|
caCertPath = destinationFile.absolutePath
|
||||||
// temporarily set the contents (don't store to settings until they decide to login)
|
advancedDialog.showRemoveCaCertificate(caCertCN!!)
|
||||||
caCertContents = content
|
|
||||||
advancedDialog.showRemoveCACertificate(name)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Utils.showSnackBar(this, getString(R.string.select_ca_failed, e.message))
|
Utils.showSnackBar(this, getString(R.string.select_ca_failed, e.message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val clientCertDialogResultLauncher =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
try {
|
||||||
|
require(result.resultCode == RESULT_OK) { "result was ${result.resultCode}" }
|
||||||
|
requireNotNull(result.data) { "file path was null" }
|
||||||
|
|
||||||
|
val uri = result.data!!.data ?: throw IllegalArgumentException("file path was null")
|
||||||
|
val fileStream = contentResolver.openInputStream(uri)
|
||||||
|
?: throw IllegalArgumentException("file path was invalid")
|
||||||
|
val destinationFile = File(filesDir, CertUtils.CLIENT_CERT_NAME)
|
||||||
|
copyStreamToFile(fileStream, destinationFile)
|
||||||
|
|
||||||
|
// temporarily store it (don't store to settings until they decide to login)
|
||||||
|
clientCertPath = destinationFile.absolutePath
|
||||||
|
advancedDialog.showRemoveClientCertificate()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Utils.showSnackBar(this, getString(R.string.select_client_failed, e.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
UncaughtExceptionHandler.registerCurrentThread()
|
UncaughtExceptionHandler.registerCurrentThread()
|
||||||
@@ -115,7 +144,7 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
binding.checkurl.visibility = View.GONE
|
binding.checkurl.visibility = View.GONE
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ClientFactory.versionApi(url, tempSslSettings())
|
ClientFactory.versionApi(settings, tempSslSettings(), url)
|
||||||
.version
|
.version
|
||||||
.enqueue(Callback.callInUI(this, onValidUrl(url), onInvalidUrl(url)))
|
.enqueue(Callback.callInUI(this, onValidUrl(url), onInvalidUrl(url)))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -140,12 +169,6 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleShowAdvanced() {
|
private fun toggleShowAdvanced() {
|
||||||
val selectedCertName = if (caCertContents != null) {
|
|
||||||
getNameOfCertContent(caCertContents!!)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
advancedDialog = AdvancedDialog(this, layoutInflater)
|
advancedDialog = AdvancedDialog(this, layoutInflater)
|
||||||
.onDisableSSLChanged { _, disable ->
|
.onDisableSSLChanged { _, disable ->
|
||||||
invalidateUrl()
|
invalidateUrl()
|
||||||
@@ -153,34 +176,53 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
.onClickSelectCaCertificate {
|
.onClickSelectCaCertificate {
|
||||||
invalidateUrl()
|
invalidateUrl()
|
||||||
doSelectCACertificate()
|
doSelectCertificate(caDialogResultLauncher, R.string.select_ca_file)
|
||||||
}
|
}
|
||||||
.onClickRemoveCaCertificate {
|
.onClickRemoveCaCertificate {
|
||||||
invalidateUrl()
|
invalidateUrl()
|
||||||
caCertContents = null
|
caCertPath = null
|
||||||
|
clientCertPassword = null
|
||||||
}
|
}
|
||||||
.show(disableSslValidation, selectedCertName)
|
.onClickSelectClientCertificate {
|
||||||
|
invalidateUrl()
|
||||||
|
doSelectCertificate(clientCertDialogResultLauncher, R.string.select_client_file)
|
||||||
|
}
|
||||||
|
.onClickRemoveClientCertificate {
|
||||||
|
invalidateUrl()
|
||||||
|
clientCertPath = null
|
||||||
|
}
|
||||||
|
.onClose { newPassword ->
|
||||||
|
clientCertPassword = newPassword
|
||||||
|
}
|
||||||
|
.show(
|
||||||
|
disableSslValidation,
|
||||||
|
caCertPath,
|
||||||
|
caCertCN,
|
||||||
|
clientCertPath,
|
||||||
|
clientCertPassword
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doSelectCACertificate() {
|
private fun doSelectCertificate(
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>,
|
||||||
|
@StringRes descriptionId: Int
|
||||||
|
) {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
// we don't really care what kind of file it is as long as we can parse it
|
// we don't really care what kind of file it is as long as we can parse it
|
||||||
intent.type = "*/*"
|
intent.type = "*/*"
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
certificateDialogResultLauncher.launch(
|
resultLauncher.launch(Intent.createChooser(intent, getString(descriptionId)))
|
||||||
Intent.createChooser(intent, getString(R.string.select_ca_file))
|
|
||||||
)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
// case for user not having a file browser installed
|
// case for user not having a file browser installed
|
||||||
Utils.showSnackBar(this, getString(R.string.please_install_file_browser))
|
Utils.showSnackBar(this, getString(R.string.please_install_file_browser))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNameOfCertContent(content: String): String {
|
private fun getNameOfCertContent(file: File): String? {
|
||||||
val ca = CertUtils.parseCertificate(content)
|
val ca = FileInputStream(file).use { CertUtils.parseCertificate(it) }
|
||||||
return (ca as X509Certificate).subjectDN.name
|
return (ca as X509Certificate).subjectX500Principal.name
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onValidUrl(url: String): SuccessCallback<VersionInfo> {
|
private fun onValidUrl(url: String): SuccessCallback<VersionInfo> {
|
||||||
@@ -211,7 +253,7 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
binding.login.visibility = View.GONE
|
binding.login.visibility = View.GONE
|
||||||
binding.loginProgress.visibility = View.VISIBLE
|
binding.loginProgress.visibility = View.VISIBLE
|
||||||
|
|
||||||
val client = ClientFactory.basicAuth(settings.url, tempSslSettings(), username, password)
|
val client = ClientFactory.basicAuth(settings, tempSslSettings(), username, password)
|
||||||
client.createService(UserApi::class.java)
|
client.createService(UserApi::class.java)
|
||||||
.currentUser()
|
.currentUser()
|
||||||
.enqueue(
|
.enqueue(
|
||||||
@@ -265,7 +307,9 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
private fun onCreatedClient(client: Client) {
|
private fun onCreatedClient(client: Client) {
|
||||||
settings.token = client.token
|
settings.token = client.token
|
||||||
settings.validateSSL = !disableSslValidation
|
settings.validateSSL = !disableSslValidation
|
||||||
settings.cert = caCertContents
|
settings.caCertPath = caCertPath
|
||||||
|
settings.clientCertPath = clientCertPath
|
||||||
|
settings.clientCertPassword = clientCertPassword
|
||||||
|
|
||||||
Utils.showSnackBar(this, getString(R.string.created_client))
|
Utils.showSnackBar(this, getString(R.string.created_client))
|
||||||
startActivity(Intent(this, InitializationActivity::class.java))
|
startActivity(Intent(this, InitializationActivity::class.java))
|
||||||
@@ -288,6 +332,17 @@ internal class LoginActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun tempSslSettings(): SSLSettings {
|
private fun tempSslSettings(): SSLSettings {
|
||||||
return SSLSettings(!disableSslValidation, caCertContents)
|
return SSLSettings(
|
||||||
|
!disableSslValidation,
|
||||||
|
caCertPath,
|
||||||
|
clientCertPath,
|
||||||
|
clientCertPassword
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyStreamToFile(inputStream: InputStream, file: File) {
|
||||||
|
FileOutputStream(file).use {
|
||||||
|
inputStream.copyTo(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ internal class MessagesActivity :
|
|||||||
|
|
||||||
private fun deleteApp(appId: Long) {
|
private fun deleteApp(appId: Long) {
|
||||||
val settings = viewModel.settings
|
val settings = viewModel.settings
|
||||||
val client = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
|
val client = ClientFactory.clientToken(settings)
|
||||||
client.createService(ApplicationApi::class.java)
|
client.createService(ApplicationApi::class.java)
|
||||||
.deleteApp(appId)
|
.deleteApp(appId)
|
||||||
.enqueue(
|
.enqueue(
|
||||||
@@ -602,8 +602,7 @@ internal class MessagesActivity :
|
|||||||
|
|
||||||
private fun deleteClientAndNavigateToLogin() {
|
private fun deleteClientAndNavigateToLogin() {
|
||||||
val settings = viewModel.settings
|
val settings = viewModel.settings
|
||||||
val api = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
|
val api = ClientFactory.clientToken(settings).createService(ClientApi::class.java)
|
||||||
.createService(ClientApi::class.java)
|
|
||||||
stopService(Intent(this@MessagesActivity, WebSocketService::class.java))
|
stopService(Intent(this@MessagesActivity, WebSocketService::class.java))
|
||||||
try {
|
try {
|
||||||
val clients = Api.execute(api.clients)
|
val clients = Api.execute(api.clients)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import com.github.gotify.messages.provider.MessageState
|
|||||||
internal class MessagesModel(parentView: Activity) : ViewModel() {
|
internal class MessagesModel(parentView: Activity) : ViewModel() {
|
||||||
val settings = Settings(parentView)
|
val settings = Settings(parentView)
|
||||||
val coilHandler = CoilHandler(parentView, settings)
|
val coilHandler = CoilHandler(parentView, settings)
|
||||||
val client = ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
|
val client = ClientFactory.clientToken(settings)
|
||||||
val appsHolder = ApplicationHolder(parentView, client)
|
val appsHolder = ApplicationHolder(parentView, client)
|
||||||
val messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
|
val messages = MessageFacade(client.createService(MessageApi::class.java), appsHolder)
|
||||||
|
|
||||||
|
|||||||
@@ -68,11 +68,7 @@ internal class WebSocketService : Service() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
settings = Settings(this)
|
settings = Settings(this)
|
||||||
val client = ClientFactory.clientToken(
|
val client = ClientFactory.clientToken(settings)
|
||||||
settings.url,
|
|
||||||
settings.sslSettings(),
|
|
||||||
settings.token
|
|
||||||
)
|
|
||||||
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
|
missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
|
||||||
Logger.info("Create ${javaClass.simpleName}")
|
Logger.info("Create ${javaClass.simpleName}")
|
||||||
coilHandler = CoilHandler(this, settings)
|
coilHandler = CoilHandler(this, settings)
|
||||||
@@ -129,7 +125,7 @@ internal class WebSocketService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchApps() {
|
private fun fetchApps() {
|
||||||
ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
|
ClientFactory.clientToken(settings)
|
||||||
.createService(ApplicationApi::class.java)
|
.createService(ApplicationApi::class.java)
|
||||||
.apps
|
.apps
|
||||||
.enqueue(
|
.enqueue(
|
||||||
|
|||||||
@@ -61,11 +61,7 @@ internal class ShareActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val client = ClientFactory.clientToken(
|
val client = ClientFactory.clientToken(settings)
|
||||||
settings.url,
|
|
||||||
settings.sslSettings(),
|
|
||||||
settings.token
|
|
||||||
)
|
|
||||||
appsHolder = ApplicationHolder(this, client)
|
appsHolder = ApplicationHolder(this, client)
|
||||||
appsHolder.onUpdate {
|
appsHolder.onUpdate {
|
||||||
val apps = appsHolder.get()
|
val apps = appsHolder.get()
|
||||||
@@ -136,11 +132,7 @@ internal class ShareActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun executeMessageCall(appIndex: Int, message: Message): Boolean {
|
private fun executeMessageCall(appIndex: Int, message: Message): Boolean {
|
||||||
val pushClient = ClientFactory.clientToken(
|
val pushClient = ClientFactory.clientToken(settings, appsHolder.get()[appIndex].token)
|
||||||
settings.url,
|
|
||||||
settings.sslSettings(),
|
|
||||||
appsHolder.get()[appIndex].token
|
|
||||||
)
|
|
||||||
return try {
|
return try {
|
||||||
val messageApi = pushClient.createService(MessageApi::class.java)
|
val messageApi = pushClient.createService(MessageApi::class.java)
|
||||||
Api.execute(messageApi.createMessage(message))
|
Api.execute(messageApi.createMessage(message))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout
|
||||||
android:orientation="vertical" android:layout_width="match_parent"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="20dp">
|
android:padding="20dp">
|
||||||
|
|
||||||
@@ -18,8 +20,47 @@
|
|||||||
android:text="@string/select_ca_certificate" />
|
android:text="@string/select_ca_certificate" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/seleceted_ca_cert"
|
android:id="@+id/selected_ca_cert"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/no_certificate_selected" />
|
android:text="@string/no_certificate_selected" />
|
||||||
|
|
||||||
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/toggle_client_cert"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:text="@string/select_client_certificate" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/selected_client_cert"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/no_certificate_selected" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/client_cert_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:maxWidth="280dp"
|
||||||
|
android:ems="10">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/client_cert_password_edittext"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:hint="@string/client_cert_password" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -40,9 +40,15 @@
|
|||||||
<string name="password">Password</string>
|
<string name="password">Password</string>
|
||||||
<string name="disabled_validate_ssl">Disable SSL Validation</string>
|
<string name="disabled_validate_ssl">Disable SSL Validation</string>
|
||||||
<string name="select_ca_certificate">Select CA Certificate</string>
|
<string name="select_ca_certificate">Select CA Certificate</string>
|
||||||
<string name="select_ca_file">Select a Certificate File</string>
|
<string name="select_client_certificate">Select Client Certificate (PKCS#12)</string>
|
||||||
|
<string name="select_ca_file">Select a CA Certificate File</string>
|
||||||
|
<string name="select_client_file">Select a Client Certificate File</string>
|
||||||
|
<string name="client_cert_password">Certificate Password</string>
|
||||||
|
<string name="client_cert_password_missing">Password required</string>
|
||||||
<string name="please_install_file_browser">Please install a file browser</string>
|
<string name="please_install_file_browser">Please install a file browser</string>
|
||||||
<string name="select_ca_failed">Failed to read CA: %s</string>
|
<string name="select_ca_failed">Failed to read CA cert: %s</string>
|
||||||
|
<string name="select_client_failed">Failed to read client cert: %s</string>
|
||||||
|
<string name="certificate_found">Certificate found</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
<string name="check_url">Check URL</string>
|
<string name="check_url">Check URL</string>
|
||||||
<string name="permissions_dialog_grant">Grant</string>
|
<string name="permissions_dialog_grant">Grant</string>
|
||||||
@@ -65,6 +71,7 @@
|
|||||||
<string name="done">Done</string>
|
<string name="done">Done</string>
|
||||||
<string name="no_certificate_selected">No certificate selected</string>
|
<string name="no_certificate_selected">No certificate selected</string>
|
||||||
<string name="remove_ca_certificate">Remove CA Certificate</string>
|
<string name="remove_ca_certificate">Remove CA Certificate</string>
|
||||||
|
<string name="remove_client_certificate">Remove Client Certificate</string>
|
||||||
<string name="warning">Warning</string>
|
<string name="warning">Warning</string>
|
||||||
<string name="http_warning">Using HTTP is insecure and it\'s recommend to use HTTPS instead. Use your favorite search engine to get more information about this topic.</string>
|
<string name="http_warning">Using HTTP is insecure and it\'s recommend to use HTTPS instead. Use your favorite search engine to get more information about this topic.</string>
|
||||||
<string name="i_understand">I Understand</string>
|
<string name="i_understand">I Understand</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user