first commit

This commit is contained in:
2025-10-10 00:16:47 +08:00
commit 7b8d189566
55 changed files with 1969 additions and 0 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
tls

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

10
.idea/deploymentTargetSelector.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AndroidLintUnsafeImplicitIntentLaunch" enabled="false" level="ERROR" enabled_by_default="false" />
</profile>
</component>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

10
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

66
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,66 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.tls"
compileSdk = 36
defaultConfig {
applicationId = "com.example.tls"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
// 网络和TLS相关依赖
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.70")
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.example.tls
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.tls", appContext.packageName)
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Tls">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Tls">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,58 @@
# 证书文件说明
请将您的证书文件放在此目录下:
- 客户端证书: `client.crt` (必需,用于客户端证书认证)
- 客户端私钥: `client.key` (必需,用于客户端证书认证)
- CA证书: `ca.crt` (可选,用于服务器证书验证)
## 证书格式要求
### 客户端证书 (client.crt)
- 格式: PEM格式的X.509证书
- 内容示例:
```
-----BEGIN CERTIFICATE-----
MIIC...证书内容...
-----END CERTIFICATE-----
```
### 客户端私钥 (client.key)
- 格式: PEM格式的PKCS#8私钥(未加密)
- 内容示例:
```
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...
-----END PRIVATE KEY-----
```
**重要**: 私钥必须是PKCS#8格式不能是PKCS#1格式RSA PRIVATE KEY
## 服务器错误解决
如果服务器报错 "peer did not return a certificate",说明服务器要求客户端证书认证。
### 解决方案:
1. 将客户端证书文件 `client.crt``client.key` 放在此目录下
2. 确保证书文件格式正确
3. 重新运行应用进行测试
### 证书生成示例使用OpenSSL
```bash
# 生成私钥PKCS#8格式
openssl genpkey -algorithm RSA -out client.key -pkcs8 -outform PEM
# 生成证书请求
openssl req -new -key client.key -out client.csr
# 生成证书(自签名)
openssl x509 -req -in client.csr -signkey client.key -out client.crt -days 365
```
### 私钥格式转换如果已有PKCS#1格式私钥
```bash
# 将PKCS#1格式转换为PKCS#8格式
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in old_key.key -out client.key
```
如果没有证书文件,应用将尝试不使用客户端证书进行连接,但可能被服务器拒绝。

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDozCCAougAwIBAgIUPJ2ri4qd4BpnUcb/6vCZ//2uYd8wDQYJKoZIhvcNAQEL
BQAwYTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl
aWppbmcxDjAMBgNVBAoMBU15T3JnMQswCQYDVQQLDAJJVDERMA8GA1UEAwwITXlS
b290Q0EwHhcNMjUxMDA3MDYwMDI3WhcNMzUxMDA1MDYwMDI3WjBhMQswCQYDVQQG
EwJDTjEQMA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEOMAwGA1UE
CgwFTXlPcmcxCzAJBgNVBAsMAklUMREwDwYDVQQDDAhNeVJvb3RDQTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMf0LkX1LF71yhsnqM4laww8B6RRo3nF
UD8pzGr4J8iFhtPyUr8T4Q3B1Et98sV+7zx4qxgYaMZ1IMX6QDti/T1qurAONhy9
/HDGDrvMYE0AOQhvRvcqicpayqNqjLBJgdzh92Uei5s8F30tqLVm8tN715UaVDuc
enjh6s9e1GpR6CzvbKLEmsUESC9YFeV1kdlbn+b5cFgDElBan2nEpS2sAUamc5pt
+h8cjwojd58o3s+Cfc69DXnbSzRVo/7DSLO0itBLNvoo1YBSmJkuGDzsu0dcUD3m
84scYvxzvOZiUqESEaqXRMPw7OcOAQSbjloUgWfiX35ntmdeK5xyk90CAwEAAaNT
MFEwHQYDVR0OBBYEFH2YOXkKFAfeuB+IXTTB5NjXFsBqMB8GA1UdIwQYMBaAFH2Y
OXkKFAfeuB+IXTTB5NjXFsBqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAA7+FwMG6yJi9vDHdaFRWA/RFSTBCo5bcFJ7DO7DwvsEOfHnP5ygulDY
astft8RsGelVSlXJtWHaCdByST9V282q+P2Q2jjwfmVB5ATq2MAgobYsU3nwLg3r
c6zd34GeTJ6wjyGF5g3rDG8NqcHlARRwL8GbWX5lbcq9ooYLl9KHgFY+gz6u5njx
WyJdnCMLRUgTt5tsT/3k5PU2V9C5kwZh/eAocEsfhuXDplFHeqdvZTZaXD/oX+So
jPnLL6fsAoCskIhVVM0xGAXtWmhk+1LVtrqCfTQrqBhtXi5qCl3NLmBf+d0OClwh
HgkcqJYr+WaiEHLUyIdfZji/ZEml7aA=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDH9C5F9Sxe9cob
J6jOJWsMPAekUaN5xVA/Kcxq+CfIhYbT8lK/E+ENwdRLffLFfu88eKsYGGjGdSDF
+kA7Yv09arqwDjYcvfxwxg67zGBNADkIb0b3KonKWsqjaoywSYHc4fdlHoubPBd9
Lai1ZvLTe9eVGlQ7nHp44erPXtRqUegs72yixJrFBEgvWBXldZHZW5/m+XBYAxJQ
Wp9pxKUtrAFGpnOabfofHI8KI3efKN7Pgn3OvQ1520s0VaP+w0iztIrQSzb6KNWA
UpiZLhg87LtHXFA95vOLHGL8c7zmYlKhEhGql0TD8OznDgEEm45aFIFn4l9+Z7Zn
XiuccpPdAgMBAAECggEAIXon7y2LxsBXHLHIqO8J26wHSYMjoCUheNnKMFSo8IEu
oDivku9EnFWJ8jO9nERS0KiRWMDpdeSxXoQ2EdtSc+B1LjnK5IgIhmcam2Wt7+Zs
JhXfZ013cWo/CBo0QOWluPIaRhNVo2Ftu1cUKn74g+D1qLCWTr61oJyOgDar0Lrn
Cf6DvZl5pGJVV3lQUiYNjZR5qEywpaMvVcVjRUaUElNplld9VLm9xE8lhwjq0GER
LQKt63p6b34kUKM8dS4cWr7+EutQPEwMLPtM/0HBUpid0qRIj5iZtROo86g8ksS1
vzGQJ5asfgiRdZX7tkrj31RDg4latXQ8saHfZeOLzwKBgQDu0jI25NKHpxbWOveS
OBtZK12aiO4n/nria3YsOTGCWYqtFUsZqcfj7/C30KH0vKODNoy9933+5roEdOvc
FIo6PDUADiZNGN3Oy69EhBVuGr4OkK2LjjCSSI2jvWONTDN4lQ17nAiiJ0LMT/TX
CBIef0i/QpAOYeuUh8FT1Ph8DwKBgQDWVkIfGLXFGeRORjUBuVv0ceQMR1K8Nz0/
ldR0nuwoxMReSgoP2OVlSXqiLWAC6zPuXxE8KBo7GkvxEc4+AcwcgPZoAkK0KGBO
BCyS6V45abp56nanAxgGZJ/BNd3MEma9rbraFjC/yF29heGeE0s+gR6uPMW32qJe
U8nBJFj1UwKBgCvxm3HEWwTA9w/GW+WY01duBlQ4G/JZ/gyJj34FrBl7FmxQvbfk
KLbFYLrB9fsNdtze/bi6wIFVvSayyO9/DAw5JdtzvxJyn+W8TuzBjRvsacpOTtCe
Akv4c6+MWrQWMGZgrtFu3ZvQs5bao4epoYPhEea3fcBXvjxfWnBtgKd7AoGAeTT8
XWN230heEFmpfhkZRCnnwX3P7rn6O+v54h1BBWkIdx29hOquBtI/tFiek+f4TROb
xn4TH1smmOPt0qjniTLwpS6qFAFFPLklj8rCywrcNjd988JPIsZihTt1+wJo8Vi+
crfbx4iCYjvEs8TLZ0RTWkrpsKfF7DvLuxpX6BsCgYBygdo3EGCuKlymw1V8xrWU
rumFLCqtTxUfTu4hzWpPM6wDBh6BNMOeXw8Z7bE69Ha8rWxGpu0qEgRM2RegNJza
f2j+hZUZbdJsGoClpq3ROBwBJHZ3ZUs+4po3JWQWc0DblaMOZftVIAh7EbMhmVPe
OwMin2eLv4aEzSEtSYK8aQ==
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDTDCCAjQCFDcqUklN76USvOskALvfIUttDamKMA0GCSqGSIb3DQEBCwUAMGEx
CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
MQ4wDAYDVQQKDAVNeU9yZzELMAkGA1UECwwCSVQxETAPBgNVBAMMCE15Um9vdENB
MB4XDTI1MTAwNzA2MDE1NFoXDTI2MTAwNzA2MDE1NFowZDELMAkGA1UEBhMCQ04x
EDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU15
T3JnMQ8wDQYDVQQLDAZDbGllbnQxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaJLDgelGrpFYgFKqGUAvopUg709OHZjwT
FyEiBic7NgWWLSne5MFBMVY18nZF9khaJZn6wN//miubkb6hbP7695GERB/wVDTj
nMd9FjDaZvYoZQkxemjCBDpyYd5vfP7f83EkNEnD2VljBbRATfl7tXxcbELDHC9N
ukkFD/3sD8IiSpZyyO1U3Ljv/vuV4UCTadJlKB+R5hNqI7QWOr59bRQD2mykXGtx
+gf44rBXqfglhY2xevuvBHYvRncHARIjOvRUmf0ue3CHPtS6wafWiPoYz6Q92xa4
xBDOtY9lrOZuwGzAsA0dRN80Uu2YmRUoe5bAnZiMk3iDBlt6dNafAgMBAAEwDQYJ
KoZIhvcNAQELBQADggEBAHV2eD8V79IF/u74mXjQxEalAJ0nA/ooMASbDIx08/wy
S4mwZl08yMFRHwV4nMp6kbRh81cWYtTe8+LW8w1mHUKd/7MzStKT93IADPULFjCi
4ee0wJmQePnWDxOcUUz6zEfh72W2Fy1CrZ99R0ZMF5ifUGUPNfmj0NsfqGgEReOO
ai3yNTEg0zg35+2SnHzodFerKWDeis5gP5EOURfHcKYKE3oEatxwn9//gb/sICoR
99rxHTO4BZB06DIArdxomZ84eanBkHGsJGuPjncR9fyLQcbP1SP0WUcR2p73xAd3
EMBByvrT1m68g7L0SVqN05EshJoYWONb4gTHd2BCO0A=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICqTCCAZECAQAwZDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAO
BgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU15T3JnMQ8wDQYDVQQLDAZDbGllbnQx
EDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCaJLDgelGrpFYgFKqGUAvopUg709OHZjwTFyEiBic7NgWWLSne5MFBMVY18nZF
9khaJZn6wN//miubkb6hbP7695GERB/wVDTjnMd9FjDaZvYoZQkxemjCBDpyYd5v
fP7f83EkNEnD2VljBbRATfl7tXxcbELDHC9NukkFD/3sD8IiSpZyyO1U3Ljv/vuV
4UCTadJlKB+R5hNqI7QWOr59bRQD2mykXGtx+gf44rBXqfglhY2xevuvBHYvRncH
ARIjOvRUmf0ue3CHPtS6wafWiPoYz6Q92xa4xBDOtY9lrOZuwGzAsA0dRN80Uu2Y
mRUoe5bAnZiMk3iDBlt6dNafAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAjnuL
FPH9DnQFg8yxNrD9XDDXGxrbRQEJqQeMJotLpafDk6Y8JROUrG9+v8p0KfdXn++6
uZHAqsYMhTej6lK7ECI4RYt1vnmZ6V8Cwj/RaY+wqUVhx3UhDrcrbhjvf4YzZIQZ
ObcFsGc6qHfjKCENQvwPP7aKxyK9/mMGbXBDhT/8wACsYRh4HL/6Rr+Uad9n7q+j
y19s6h+SkkGST9feE4X4dWCx87w5us9QTRIywD4/L8IlzJXGEhZ285QxOokw/R7t
pQCkKksycajW7cu7kIZOsTus9wW2vyBhsfJnzTfZq8Qrqyt53C1d431Z8uD9yLMa
SfxqqgmsSUSLjfw3nA==
-----END CERTIFICATE REQUEST-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaJLDgelGrpFYg
FKqGUAvopUg709OHZjwTFyEiBic7NgWWLSne5MFBMVY18nZF9khaJZn6wN//miub
kb6hbP7695GERB/wVDTjnMd9FjDaZvYoZQkxemjCBDpyYd5vfP7f83EkNEnD2Vlj
BbRATfl7tXxcbELDHC9NukkFD/3sD8IiSpZyyO1U3Ljv/vuV4UCTadJlKB+R5hNq
I7QWOr59bRQD2mykXGtx+gf44rBXqfglhY2xevuvBHYvRncHARIjOvRUmf0ue3CH
PtS6wafWiPoYz6Q92xa4xBDOtY9lrOZuwGzAsA0dRN80Uu2YmRUoe5bAnZiMk3iD
Blt6dNafAgMBAAECggEAHzak1qAOX7qEcjSdH1ugPbkSeoL3h6iNK7R4UiJ62UOk
N/fnTap289OgyIXTq3Emz5JjruJVubWndPY7awbeT0XIoscEzK7QkvLRdqQCuoc0
+5MSHIHUKs2eZEErQNpH5mOumo04Dr+5mRKzoH3pskJa74BAuK/BaHT7ilnlqmJp
xoVDilQC5jiGys4MpCtx2Kt8l2CAlsE84vZff+PGV4O0kDsqqky6uhuJtrTaZs7j
bFJoWcG9L9s/WZtwb32X11epHOmQv6mrzgFyminzkBoCmO7cBNB44eyQJnnNYece
MnFdklKCPuSYc0lgWSepuUkrz8hLKC8+QzQO668UsQKBgQDYfmM9ES087q+H9ooc
l/GZsEdV/iN+Kcm1JkyyypzRQrmVUTyAp9SFRaLiQP3ea6Jy/CUeCC1SWwPLJM9E
EA0jlCYIPFOhsT7I1EM5Qa7DWf1z8bAVUMs1cqZrzOegIbEiKSu+Ym4MMVtvKvON
OqaTtwwNUvyxVHmsuZBUZzzXOQKBgQC2RZLvI4p/Qzbpznsmq4beCSFEACRJTa/F
e58/Xbmfdwo42WU17548k7sN/Jgyg7PVu4+bY5ZAYMibXFtEHW+sBz/4Gf90BZxE
egiKD7gtEw4kxu8BfygJgWyszK4guzTYKx+DJRl2xtvGCNX10SrV1rEfHvYNHXj8
BekIht0ElwKBgQCb/FSch2fE42Vt3WEdwQy+45hCiV4hZRKEhxf0KrBaxmzY/TNO
r54ceFQoGRPR0lO17Z8AyHt/Pzy4fckpDTeqTvAoNu87LW5DXU0iUAUPlCNeCuII
ObJwzC7EtVqesifiqS9veZQ5DMcIjjX1qDClddolL4oKawdQQFORvODFYQKBgB/l
FM0b3wRd8qH/K7WclkEMP/HyRGc/XN6lvzwLXov0/KjuAbPqdjoLb9QGu2s7eKCR
7ZM3Xfdt+CyXgLDupbfonN0BT54xzSJ+aDgggA4DI5pz5SbR5WOkbivetSmtGJYr
FZyRRV9vdM22hho5u9EnfF8Bv/STj7QqJJkFYG+JAoGBAKx9evvrTS1tgcduIZXm
fFQJ/Y4r2AtUqPs5TE69wdQg+WL35Njd0r3gguK4isur4NV3Yb97JUN95L9E2cY2
asRkLNGiWwgFtSp6xTA6ltfLbdW+smZgeFkMTu8O6WBUiep6TEkRiR60UnKxDfwO
eNr+BzEVtLNmY43pf3uRjnfm
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,203 @@
package com.example.tls
import android.content.Context
import android.util.Log
import java.io.InputStream
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import javax.net.ssl.X509KeyManager
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSession
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.RSAPrivateCrtKeySpec
import java.util.Base64
import java.io.ByteArrayInputStream
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import java.io.StringReader
class CertificateManager(private val context: Context) {
fun loadCertificateFromAssets(fileName: String): X509Certificate? {
return try {
val inputStream: InputStream = context.assets.open(fileName)
val certificateFactory = CertificateFactory.getInstance("X.509")
certificateFactory.generateCertificate(inputStream) as X509Certificate
} catch (e: Exception) {
Log.e("CertificateManager", "加载证书失败: ${e.message}")
null
}
}
fun createTrustManagerWithCustomCert(certificate: X509Certificate): X509TrustManager {
return object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
// 客户端证书验证
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
// 服务器证书验证 - 这里可以添加自定义验证逻辑
for (cert in chain) {
if (cert == certificate) {
return // 信任我们的自定义证书
}
}
// 如果不匹配,可以选择抛出异常或使用默认验证
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf(certificate)
}
}
}
fun createDefaultTrustManager(): X509TrustManager {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
return trustManagerFactory.trustManagers[0] as X509TrustManager
}
fun createCustomTrustManager(): X509TrustManager {
return object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
// 接受所有客户端证书
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
// 接受所有服务器证书 - 仅用于测试
Log.d("CertificateManager", "接受服务器证书: ${chain[0].subjectDN}")
Log.d("CertificateManager", "证书颁发者: ${chain[0].issuerDN}")
Log.d("CertificateManager", "证书有效期: ${chain[0].notBefore}${chain[0].notAfter}")
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
}
fun loadPrivateKeyFromAssets(fileName: String): PrivateKey? {
return try {
val inputStream: InputStream = context.assets.open(fileName)
val keyContent = inputStream.readBytes().toString(Charsets.UTF_8)
// 首先尝试简单的Base64解码适用于PKCS#8格式
if (keyContent.contains("-----BEGIN PRIVATE KEY-----")) {
Log.d("CertificateManager", "检测到PKCS#8格式私钥使用标准解析")
val keyBytes = extractKeyBytes(keyContent, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----")
val keySpec = PKCS8EncodedKeySpec(keyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
keyFactory.generatePrivate(keySpec)
} else if (keyContent.contains("-----BEGIN RSA PRIVATE KEY-----")) {
Log.d("CertificateManager", "检测到PKCS#1格式私钥建议转换为PKCS#8格式")
Log.e("CertificateManager", "PKCS#1格式私钥需要转换请运行转换脚本")
Log.e("CertificateManager", "Linux/Mac: ./convert_private_key.sh")
Log.e("CertificateManager", "Windows: convert_private_key.bat")
null
} else {
Log.e("CertificateManager", "无法识别私钥格式")
Log.e("CertificateManager", "请确保私钥是PEM格式PKCS#8或PKCS#1")
null
}
} catch (e: Exception) {
Log.e("CertificateManager", "加载私钥失败: ${e.message}")
Log.e("CertificateManager", "错误类型: ${e.javaClass.simpleName}")
// 提供更详细的错误信息
when {
e.message?.contains("parsing") == true -> {
Log.e("CertificateManager", "私钥格式错误请检查PEM格式是否正确")
}
e.message?.contains("encrypted") == true -> {
Log.e("CertificateManager", "私钥已加密,请使用未加密的私钥")
}
e.message?.contains("PKCS8") == true -> {
Log.e("CertificateManager", "请使用PKCS#8格式的私钥")
}
else -> {
Log.e("CertificateManager", "请确保私钥是PEM格式且未加密")
}
}
null
}
}
private fun extractKeyBytes(keyContent: String, beginMarker: String, endMarker: String): ByteArray {
val lines = keyContent.split("\n")
val keyLines = lines.filter {
!it.startsWith("-----") && it.trim().isNotEmpty()
}
val keyString = keyLines.joinToString("")
return Base64.getDecoder().decode(keyString)
}
fun createKeyManager(certificate: X509Certificate, privateKey: PrivateKey): X509KeyManager {
return object : X509KeyManager {
override fun chooseClientAlias(keyType: Array<String>, issuers: Array<java.security.Principal>, socket: java.net.Socket): String {
return "client"
}
override fun chooseServerAlias(keyType: String, issuers: Array<java.security.Principal>, socket: java.net.Socket): String? {
return null
}
override fun getCertificateChain(alias: String): Array<X509Certificate> {
return arrayOf(certificate)
}
override fun getClientAliases(keyType: String, issuers: Array<java.security.Principal>): Array<String> {
return arrayOf("client")
}
override fun getServerAliases(keyType: String, issuers: Array<java.security.Principal>): Array<String> {
return arrayOf()
}
override fun getPrivateKey(alias: String): PrivateKey {
return privateKey
}
}
}
fun createKeyManagerFactory(): KeyManagerFactory? {
return try {
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(null, null)
keyManagerFactory
} catch (e: Exception) {
Log.e("CertificateManager", "创建KeyManagerFactory失败: ${e.message}")
null
}
}
fun createCustomHostnameVerifier(): HostnameVerifier {
return HostnameVerifier { hostname, session ->
Log.d("CertificateManager", "验证主机名: $hostname")
Log.d("CertificateManager", "SSL会话: ${session.protocol}")
// 对于IP地址跳过主机名验证
if (hostname.matches(Regex("^\\d+\\.\\d+\\.\\d+\\.\\d+$"))) {
Log.d("CertificateManager", "IP地址主机名验证通过: $hostname")
return@HostnameVerifier true
}
// 对于域名,进行标准验证
try {
val result = javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)
Log.d("CertificateManager", "域名验证结果: $result")
result
} catch (e: Exception) {
Log.w("CertificateManager", "主机名验证异常: ${e.message}")
// 对于测试环境,允许所有主机名
true
}
}
}
}

View File

@@ -0,0 +1,338 @@
package com.example.tls
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.tls.ui.theme.TlsTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private lateinit var tlsTestService: TlsTestService
// 可配置的测试服务器列表
private val testServers = listOf(
// "https://httpbin.org/post",
// "https://www.google.com",
// "https://www.cloudflare.com",
// "https://www.github.com",
"https://43.162.113.116:7271", // 示例IP+端口
// "https://10.0.0.1:443", // 示例内网IP
// "https://localhost:8080" // 示例本地服务器
)
private var currentServerIndex = 0
private var customServerUrl by mutableStateOf("")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
tlsTestService = TlsTestService(this)
setContent {
TlsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TlsTestScreen(
modifier = Modifier.padding(innerPadding),
tlsTestService = tlsTestService,
serverUrl = if (customServerUrl.isNotEmpty()) customServerUrl else testServers[currentServerIndex],
testServers = testServers,
customServerUrl = customServerUrl,
onServerChange = { index ->
currentServerIndex = index
customServerUrl = ""
},
onCustomServerChange = { url -> customServerUrl = url }
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TlsTestScreen(
modifier: Modifier = Modifier,
tlsTestService: TlsTestService,
serverUrl: String,
testServers: List<String> = emptyList(),
customServerUrl: String = "",
onServerChange: (Int) -> Unit = {},
onCustomServerChange: (String) -> Unit = {}
) {
var logs by remember { mutableStateOf(listOf<String>()) }
var isTestRunning by remember { mutableStateOf(false) }
var tls12Result by remember { mutableStateOf<TestResult?>(null) }
var tls13Result by remember { mutableStateOf<TestResult?>(null) }
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
// 自动滚动到最新日志
LaunchedEffect(logs.size) {
if (logs.isNotEmpty()) {
listState.animateScrollToItem(logs.size - 1)
}
}
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
// 标题
Text(
text = "TLS 1.2/1.3 循环测试",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
// 服务器选择
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F0F0))
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "测试服务器",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
modifier = Modifier.padding(bottom = 8.dp)
)
// 自定义服务器输入
OutlinedTextField(
value = customServerUrl,
onValueChange = onCustomServerChange,
label = { Text("自定义服务器 (IP:端口)") },
placeholder = { Text("例如: https://192.168.1.100:8443") },
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = KeyboardType.Uri
),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
singleLine = true
)
// 预设服务器列表
if (testServers.isNotEmpty()) {
Text(
text = "预设服务器:",
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
modifier = Modifier.padding(bottom = 4.dp)
)
testServers.forEachIndexed { index, server ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = customServerUrl.isEmpty() && server == serverUrl,
onClick = { onServerChange(index) }
)
Text(
text = server,
modifier = Modifier.padding(start = 8.dp),
fontSize = 12.sp
)
}
}
}
// 当前选择的服务器显示
if (serverUrl.isNotEmpty()) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFE3F2FD))
) {
Text(
text = "当前服务器: $serverUrl",
modifier = Modifier.padding(12.dp),
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF1976D2)
)
}
}
}
}
// 控制按钮
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = {
if (!isTestRunning) {
isTestRunning = true
logs = listOf("开始TLS测试...")
tls12Result = null
tls13Result = null
scope.launch {
tlsTestService.startContinuousTest(
serverUrl = serverUrl,
onLog = { log ->
logs = logs + log
},
onTestComplete = { tls12, tls13 ->
tls12Result = tls12
tls13Result = tls13
}
)
}
}
},
enabled = !isTestRunning,
modifier = Modifier.weight(1f)
) {
Text("开始测试")
}
Button(
onClick = {
isTestRunning = false
logs = logs + "测试已停止"
},
enabled = isTestRunning,
modifier = Modifier.weight(1f)
) {
Text("停止测试")
}
}
Spacer(modifier = Modifier.height(16.dp))
// 测试结果显示
if (tls12Result != null || tls13Result != null) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF5F5F5))
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "最新测试结果",
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
modifier = Modifier.padding(bottom = 8.dp)
)
tls12Result?.let { result ->
TestResultItem("TLS 1.2", result)
}
tls13Result?.let { result ->
TestResultItem("TLS 1.3", result)
}
}
}
}
// 日志显示区域
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
colors = CardDefaults.cardColors(containerColor = Color.Black)
) {
Column {
Text(
text = "测试日志",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp)
)
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(logs) { log ->
Text(
text = log,
color = Color.Green,
fontSize = 12.sp,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
}
}
}
}
}
@Composable
fun TestResultItem(protocol: String, result: TestResult) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = protocol,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
)
when (result) {
is TestResult.Success -> {
Text(
text = "成功 - ${result.duration}ms",
color = Color.Green,
fontWeight = FontWeight.Medium
)
}
is TestResult.Failure -> {
Text(
text = "失败 - ${result.error}",
color = Color.Red,
fontWeight = FontWeight.Medium
)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun TlsTestScreenPreview() {
TlsTheme {
// Preview中不显示实际功能
Text("TLS测试应用预览")
}
}

View File

@@ -0,0 +1,279 @@
package com.example.tls
import android.content.Context
import android.util.Log
import kotlinx.coroutines.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.security.cert.X509Certificate
import javax.net.ssl.*
import java.util.concurrent.TimeUnit
class TlsTestService(private val context: Context) {
private val certificateManager = CertificateManager(context)
// 尝试加载客户端证书
private val clientCertificate = certificateManager.loadCertificateFromAssets("client.crt")
private val clientPrivateKey = certificateManager.loadPrivateKeyFromAssets("client.key")
private val hasClientCert = clientCertificate != null && clientPrivateKey != null
init {
// 在初始化时记录证书加载状态
if (hasClientCert) {
Log.d("TlsTestService", "客户端证书加载成功")
} else {
Log.w("TlsTestService", "客户端证书加载失败或未找到")
if (clientCertificate == null) Log.w("TlsTestService", "证书文件 client.crt 未找到或格式错误")
if (clientPrivateKey == null) Log.w("TlsTestService", "私钥文件 client.key 未找到或格式错误")
}
}
// 为每次测试创建新的客户端避免SSL会话重用问题
private fun createTls12Client(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.sslSocketFactory(
createTls12SocketFactory(),
certificateManager.createCustomTrustManager()
)
.hostnameVerifier(certificateManager.createCustomHostnameVerifier())
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) // 禁用连接池
.build()
}
private fun createTls13Client(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.sslSocketFactory(
createTls13SocketFactory(),
certificateManager.createCustomTrustManager()
)
.hostnameVerifier(certificateManager.createCustomHostnameVerifier())
.connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) // 禁用连接池
.build()
}
private fun createTls12SocketFactory(): SSLSocketFactory {
val sslContext = SSLContext.getInstance("TLSv1.2")
val keyManagers = if (hasClientCert) {
arrayOf(certificateManager.createKeyManager(clientCertificate!!, clientPrivateKey!!))
} else {
null
}
sslContext.init(keyManagers, arrayOf(certificateManager.createCustomTrustManager()), null)
return sslContext.socketFactory
}
private fun createTls13SocketFactory(): SSLSocketFactory {
val sslContext = SSLContext.getInstance("TLSv1.3")
val keyManagers = if (hasClientCert) {
arrayOf(certificateManager.createKeyManager(clientCertificate!!, clientPrivateKey!!))
} else {
null
}
sslContext.init(keyManagers, arrayOf(certificateManager.createCustomTrustManager()), null)
return sslContext.socketFactory
}
suspend fun testTls12(serverUrl: String, onLog: (String) -> Unit): TestResult {
return withContext(Dispatchers.IO) {
var response: okhttp3.Response? = null
try {
onLog("开始TLS 1.2测试...")
onLog("目标服务器: $serverUrl")
onLog("客户端证书状态: ${if (hasClientCert) "已加载" else "未找到证书文件"}")
val testData = createTestData()
onLog("准备发送数据: ${testData.length} 字节")
logTestDataInfo(testData, onLog)
val startTime = System.currentTimeMillis()
val request = Request.Builder()
.url(serverUrl)
.post(testData.toRequestBody("text/plain".toMediaType()))
.addHeader("Content-Type", "text/plain")
.addHeader("User-Agent", "TLS-Test-Client/1.0")
.build()
onLog("正在建立TLS 1.2连接...")
onLog("目标主机: ${java.net.URL(serverUrl).host}")
val tls12Client = createTls12Client() // 创建新的客户端实例
response = tls12Client.newCall(request).execute()
val endTime = System.currentTimeMillis()
val duration = endTime - startTime
if (response.isSuccessful) {
val responseBody = response.body?.string() ?: ""
onLog("TLS 1.2连接成功 - 耗时: ${duration}ms")
onLog("已发送数据: ${testData.length} 字节")
onLog("服务器响应长度: ${responseBody.length} 字节")
onLog("TLS版本: ${response.protocol}")
onLog("HTTP状态: ${response.code}")
logResponseInfo(responseBody, onLog)
TestResult.Success(duration, responseBody.length)
} else {
onLog("TLS 1.2测试失败 - HTTP ${response.code}")
onLog("响应头: ${response.headers}")
TestResult.Failure("HTTP ${response.code}")
}
} catch (e: Exception) {
onLog("TLS 1.2测试异常: ${e.message}")
onLog("异常类型: ${e.javaClass.simpleName}")
TestResult.Failure(e.message ?: "未知错误")
} finally {
// 确保连接被正确关闭
try {
response?.close()
} catch (e: Exception) {
Log.d("TlsTestService", "关闭连接时发生异常: ${e.message}")
}
}
}
}
suspend fun testTls13(serverUrl: String, onLog: (String) -> Unit): TestResult {
return withContext(Dispatchers.IO) {
var response: okhttp3.Response? = null
try {
onLog("开始TLS 1.3测试...")
onLog("目标服务器: $serverUrl")
onLog("客户端证书状态: ${if (hasClientCert) "已加载" else "未找到证书文件"}")
val testData = createTestData()
onLog("准备发送数据: ${testData.length} 字节")
logTestDataInfo(testData, onLog)
val startTime = System.currentTimeMillis()
val request = Request.Builder()
.url(serverUrl)
.post(testData.toRequestBody("text/plain".toMediaType()))
.addHeader("Content-Type", "text/plain")
.addHeader("User-Agent", "TLS-Test-Client/1.0")
.build()
onLog("正在建立TLS 1.3连接...")
onLog("目标主机: ${java.net.URL(serverUrl).host}")
val tls13Client = createTls13Client() // 创建新的客户端实例
response = tls13Client.newCall(request).execute()
val endTime = System.currentTimeMillis()
val duration = endTime - startTime
if (response.isSuccessful) {
val responseBody = response.body?.string() ?: ""
onLog("TLS 1.3连接成功 - 耗时: ${duration}ms")
onLog("已发送数据: ${testData.length} 字节")
onLog("服务器响应长度: ${responseBody.length} 字节")
onLog("TLS版本: ${response.protocol}")
onLog("HTTP状态: ${response.code}")
logResponseInfo(responseBody, onLog)
TestResult.Success(duration, responseBody.length)
} else {
onLog("TLS 1.3测试失败 - HTTP ${response.code}")
onLog("响应头: ${response.headers}")
TestResult.Failure("HTTP ${response.code}")
}
} catch (e: Exception) {
onLog("TLS 1.3测试异常: ${e.message}")
onLog("异常类型: ${e.javaClass.simpleName}")
TestResult.Failure(e.message ?: "未知错误")
} finally {
// 确保连接被正确关闭
try {
response?.close()
} catch (e: Exception) {
Log.d("TlsTestService", "关闭连接时发生异常: ${e.message}")
}
}
}
}
private fun create1KDataRequestBody(): RequestBody {
val data = "A".repeat(1024) // 创建1KB的数据
return data.toRequestBody("text/plain".toMediaType())
}
private fun createTestData(): String {
// 创建包含时间戳的测试数据,确保每次发送的数据都不同
val timestamp = System.currentTimeMillis()
val testData = "TLS_TEST_DATA_${timestamp}_"
val remainingSize = 1024 - testData.length
val padding = "A".repeat(remainingSize.coerceAtLeast(0))
return testData + padding
}
private fun logTestDataInfo(testData: String, onLog: (String) -> Unit) {
onLog("测试数据详情:")
onLog(" - 数据长度: ${testData.length} 字节")
onLog(" - 数据前缀: ${testData.take(50)}...")
onLog(" - 数据后缀: ...${testData.takeLast(20)}")
}
private fun logResponseInfo(responseBody: String, onLog: (String) -> Unit) {
onLog("服务器响应详情:")
onLog(" - 响应长度: ${responseBody.length} 字节")
if (responseBody.isNotEmpty()) {
onLog(" - 响应前缀: ${responseBody.take(100)}...")
if (responseBody.length > 100) {
onLog(" - 响应后缀: ...${responseBody.takeLast(50)}")
}
}
}
private fun clearSSLSession() {
try {
// 清理SSL会话缓存
val sslContext = SSLContext.getDefault()
val sessionContext = sslContext.clientSessionContext
sessionContext.setSessionCacheSize(0)
sessionContext.setSessionTimeout(0)
Log.d("TlsTestService", "SSL会话缓存已清理")
} catch (e: Exception) {
Log.d("TlsTestService", "清理SSL会话时发生异常: ${e.message}")
}
}
fun startContinuousTest(
serverUrl: String,
onLog: (String) -> Unit,
onTestComplete: (TestResult, TestResult) -> Unit
) {
CoroutineScope(Dispatchers.Main).launch {
while (true) {
try {
onLog("=== 开始新一轮测试 ===")
// 测试TLS 1.2
val tls12Result = testTls12(serverUrl, onLog)
// 测试TLS 1.3
val tls13Result = testTls13(serverUrl, onLog)
onTestComplete(tls12Result, tls13Result)
// 清理SSL会话缓存避免会话重用问题
clearSSLSession()
onLog("等待3秒后进行下一轮测试...")
delay(3000)
} catch (e: Exception) {
onLog("测试过程中发生异常: ${e.message}")
delay(3000)
}
}
}
}
}
sealed class TestResult {
data class Success(val duration: Long, val responseSize: Int) : TestResult()
data class Failure(val error: String) : TestResult()
}

View File

@@ -0,0 +1,11 @@
package com.example.tls.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,58 @@
package com.example.tls.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun TlsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,34 @@
package com.example.tls.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">tls</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Tls" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.example.tls
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

6
build.gradle.kts Normal file
View File

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

23
gradle.properties Normal file
View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

32
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,32 @@
[versions]
agp = "8.12.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Thu Oct 09 21:54:23 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Normal file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

23
settings.gradle.kts Normal file
View File

@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "tls"
include(":app")