owenclave / app/src/main/java/io/nekohasekai/sagernet/ui/ProbeCertActivity.kt

github - dev - back to tree - upstream
/****************************************************************************** * * * Copyright (C) 2024 dyhkwong * * Copyright (C) 2024 HystericalDragons * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ******************************************************************************/ package io.nekohasekai.sagernet.ui import android.annotation.SuppressLint import android.content.ClipData import android.graphics.Typeface import android.os.Bundle import android.view.MenuItem import android.view.View import android.widget.AdapterView import android.widget.HorizontalScrollView import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.core.widget.NestedScrollView import androidx.core.widget.doAfterTextChanged import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.SagerNet import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.databinding.LayoutProbeCertBinding import io.nekohasekai.sagernet.ktx.Logs import io.nekohasekai.sagernet.ktx.dp2px import io.nekohasekai.sagernet.ktx.onMainDispatcher import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher import io.nekohasekai.sagernet.ktx.runOnMainDispatcher import libexclavecore.Libexclavecore class ProbeCertActivity : ThemedActivity() { private lateinit var binding: LayoutProbeCertBinding @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = LayoutProbeCertBinding.inflate(layoutInflater) setContentView(binding.root) ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) v.updatePadding( bottom = ime.bottom, ) insets } ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_layout)) { v, insets -> val bars = insets.getInsets( WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() ) val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) v.updatePadding( left = bars.left, right = bars.right, bottom = if (ime.bottom > bars.bottom) 0 else bars.bottom - ime.bottom, ) insets } setSupportActionBar(findViewById(R.id.toolbar)) supportActionBar?.apply { setTitle(R.string.probe_cert) setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(R.drawable.ic_navigation_close) } binding.probeCertServer.setText("example.com") binding.probeCertServerPort.setText("443") binding.probeCertServerName.setText("example.com") binding.probeCertServer.doAfterTextChanged { text -> binding.probeCertServerName.setText(text) } binding.probeCertAlpn.setText("h2,http/1.1") binding.probeCertProtocol.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long ) { when (position) { 0 -> binding.probeCertAlpn.setText("h2,http/1.1") 1 -> binding.probeCertAlpn.setText("h3") else -> error("unknown protocol") } } override fun onNothingSelected(parent: AdapterView<*>?) { } } binding.probeCert.setOnClickListener { probeCert() } binding.certificate.doAfterTextChanged { text -> try { binding.certHash.text = when (binding.certHashType.selectedItemPosition) { 0 -> Libexclavecore.calculatePEMCertSHA256Hash(text.toString()) 1 -> Libexclavecore.calculatePEMCertPublicKeySHA256Hash(text.toString()) 2 -> Libexclavecore.calculatePEMCertChainSHA256Hash(text.toString()) else -> error("impossible") } binding.showCertInfo.isVisible = true } catch (_: Exception) { binding.certHash.text = "" binding.showCertInfo.isVisible = false } } binding.certHashType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long ) { try { val certificate = binding.certificate.text.toString() val certHash = when (position) { 0 -> Libexclavecore.calculatePEMCertSHA256Hash(certificate) 1 -> Libexclavecore.calculatePEMCertPublicKeySHA256Hash(certificate) 2 -> Libexclavecore.calculatePEMCertChainSHA256Hash(certificate) else -> error("impossible") } binding.certHash.text = certHash } catch (_: Exception) { binding.certHash.text = "" } } override fun onNothingSelected(parent: AdapterView<*>?) { } } binding.certHash.setOnClickListener { try { val clipData = ClipData.newPlainText("hash", binding.certHash.text) SagerNet.clipboard.setPrimaryClip(clipData) } catch (e: Exception) { Logs.w(e) } } binding.showCertInfo.text = "?" binding.showCertInfo.isVisible = false binding.showCertInfo.setOnClickListener { try { AlertDialog.Builder(this) .setTitle(R.string.certificates) .setView(NestedScrollView(this).apply { setPadding(dp2px(16), dp2px(16), dp2px(16), 0) addView( HorizontalScrollView(this@ProbeCertActivity).apply { addView(TextView(this@ProbeCertActivity).apply { text = Libexclavecore.certificateToPrettyInfo(binding.certificate.text.toString()) isSingleLine = false typeface = Typeface.MONOSPACE setTextIsSelectable(true) setHorizontallyScrolling(false) }) }) }) .setPositiveButton(android.R.string.ok) { _, _ -> } .runCatching { show() } } catch (e: Exception) { AlertDialog.Builder(this@ProbeCertActivity) .setTitle(R.string.error_title) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok) { _, _ -> } .runCatching { show() } } } } private fun probeCert() { var port: Int try { port = binding.probeCertServerPort.text.toString().toInt() } catch (e: NumberFormatException) { binding.waitLayout.isVisible = false binding.certificate.setText("") runOnMainDispatcher { AlertDialog.Builder(this@ProbeCertActivity) .setTitle(R.string.error_title) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok) { _, _ -> } .runCatching { show() } } return } binding.waitLayout.isVisible = true runOnDefaultDispatcher { val certProber = Libexclavecore.newCertProber().apply { if (SagerNet.started && DataStore.startedProfile > 0) { useUDS(SagerNet.deviceStorage.noBackupFilesDir.toString() + "/ipc.sock") } } val result = when (binding.probeCertProtocol.selectedItemPosition) { 0 -> certProber.probeTLS( binding.probeCertServer.text.toString(), port, binding.probeCertServerName.text.toString(), binding.probeCertAlpn.text.toString(), ) 1 -> certProber.probeQUIC( binding.probeCertServer.text.toString(), port, binding.probeCertServerName.text.toString(), binding.probeCertAlpn.text.toString(), ) else -> error("impossible") } onMainDispatcher { binding.waitLayout.isVisible = false if (result.error.isNotEmpty()) { binding.certificate.setText("") AlertDialog.Builder(this@ProbeCertActivity) .setTitle(R.string.error_title) .setMessage(result.error) .setPositiveButton(android.R.string.ok) { _, _ -> } .runCatching { show() } } else { binding.certificate.setText(result.cert) if (result.verifyError.isNotEmpty()) { AlertDialog.Builder(this@ProbeCertActivity) .setTitle(R.string.error_title) .setMessage(result.verifyError) .setPositiveButton(android.R.string.ok) { _, _ -> } .runCatching { show() } } } } } } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { finishAndRemoveTask() true } else -> super.onOptionsItemSelected(item) } } }