[feat] Server Configure& Https

This commit is contained in:
acite
2025-08-26 02:10:35 +08:00
parent ded0386419
commit b74ca98bf9
6 changed files with 289 additions and 77 deletions

View File

@@ -1,25 +1,101 @@
package com.acitelight.aether.service
import android.content.Context
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.CertificatePinner
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.ByteArrayInputStream
import java.security.KeyStore
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
object ApiClient {
const val base: String = "http://192.168.1.213/"
var base: String = ""
var domain: String = ""
var cert: String = ""
private val json = Json {
ignoreUnknownKeys = true
}
private val retrofit = Retrofit.Builder()
.baseUrl(base)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
fun loadCertificateFromString(pemString: String): X509Certificate {
val certificateFactory = CertificateFactory.getInstance("X.509")
val decodedPem = pemString
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replace("\\s+".toRegex(), "")
val api: ApiInterface by lazy {
retrofit.create(ApiInterface::class.java)
val decodedBytes = android.util.Base64.decode(decodedPem, android.util.Base64.DEFAULT)
ByteArrayInputStream(decodedBytes).use { inputStream ->
return certificateFactory.generateCertificate(inputStream) as X509Certificate
}
}
fun createOkHttpClientWithDynamicCert(trustedCert: X509Certificate): OkHttpClient {
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null, null)
setCertificateEntry("ca", trustedCert)
}
val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
val trustManager = tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
val sslContext = SSLContext.getInstance("TLS").apply {
init(null, arrayOf(trustManager), null)
}
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
} catch (e: Exception) {
throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e)
}
}
private fun createRetrofit(): Retrofit {
val okHttpClient = createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
return Retrofit.Builder()
.baseUrl(base)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
}
var api: ApiInterface? = null
fun apply(url: String, crt: String)
{
try{
domain = url.toHttpUrlOrNull()?.host !!
cert = crt
base = url
api = createRetrofit().create(ApiInterface::class.java)
}catch (e: Exception)
{
api = null
base = ""
domain = ""
cert = ""
}
}
}

View File

@@ -9,12 +9,12 @@ import java.security.PrivateKey
import java.security.Signature
object AuthManager {
suspend fun fetchToken(baseUrl: String, username: String, privateKey: String): String? {
suspend fun fetchToken(username: String, privateKey: String): String? {
val api = ApiClient.api
var challengeBase64 = ""
try{
challengeBase64 = api.getChallenge(username).string()
challengeBase64 = api!!.getChallenge(username).string()
}catch (e: Exception)
{
print(e.message)
@@ -23,7 +23,7 @@ object AuthManager {
val signedBase64 = signChallenge(db64(privateKey), db64(challengeBase64))
return try {
api.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string()
api!!.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string()
} catch (e: Exception) {
e.printStackTrace()
null

View File

@@ -15,7 +15,7 @@ object MediaManager
{
try
{
val j = ApiClient.api.getVideoClasses(token)
val j = ApiClient.api!!.getVideoClasses(token)
return j.toList()
}catch(e: Exception)
{
@@ -26,7 +26,7 @@ object MediaManager
suspend fun listVideos(klass: String): List<Video>
{
try {
val j = ApiClient.api.queryVideoClasses(klass, token)
val j = ApiClient.api!!.queryVideoClasses(klass, token)
return j.map{
queryVideo(klass, it)!!
}.toList()
@@ -39,7 +39,7 @@ object MediaManager
suspend fun queryVideo(klass: String, id: String): Video?
{
try {
val j = ApiClient.api.queryVideo(klass, id, token)
val j = ApiClient.api!!.queryVideo(klass, id, token)
return Video(klass = klass, id = id, token=token, j)
}catch (e: Exception)
{
@@ -49,11 +49,13 @@ object MediaManager
suspend fun listComics() : List<String>
{
return ApiClient.api.getComicCollections()
// TODO: try
return ApiClient.api!!.getComicCollections()
}
suspend fun queryComicInfo(c: String) : Comic
{
return ApiClient.api.queryComicInfo(c)
// TODO: try
return ApiClient.api!!.queryComicInfo(c)
}
}

View File

@@ -8,10 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
@@ -25,19 +28,22 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.acitelight.aether.viewModel.MeScreenViewModel
@Composable
fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel())
{
fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel()) {
val context = LocalContext.current
var username by meScreenViewModel.username;
var privateKey by meScreenViewModel.privateKey;
var url by meScreenViewModel.url
var cert by meScreenViewModel.cert
Column(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
@@ -45,62 +51,130 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel())
verticalArrangement = Arrangement.Top
) {
// Card component for a clean, contained UI block
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Column(
item{
Card(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Account Setting",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp).align(Alignment.Start)
)
// Username input field
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = "Username")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Private key input field
OutlinedTextField(
value = privateKey,
onValueChange = { privateKey = it },
label = { Text("Key") },
leadingIcon = {
Icon(Icons.Default.Key, contentDescription = "Key")
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Save Button
Button(
onClick = {
meScreenViewModel.updateAccount(username, privateKey, context)
},
modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.padding(8.dp)
)
{
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Save")
Text(
text = "Account Setting",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.Start)
)
// Username input field
OutlinedTextField(
value = username,
onValueChange = { username = it },
label = { Text("Username") },
leadingIcon = {
Icon(Icons.Default.Person, contentDescription = "Username")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Private key input field
OutlinedTextField(
value = privateKey,
onValueChange = { privateKey = it },
label = { Text("Key") },
leadingIcon = {
Icon(Icons.Default.Key, contentDescription = "Key")
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// Save Button
Button(
onClick = {
meScreenViewModel.updateAccount(username, privateKey, context)
},
modifier = Modifier.fillMaxWidth()
) {
Text("Save")
}
}
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
{
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Server Setting",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.padding(bottom = 16.dp)
.align(Alignment.Start)
)
// Username input field
OutlinedTextField(
value = url,
onValueChange = { url = it },
label = { Text("Url") },
leadingIcon = {
Icon(Icons.Default.Link, contentDescription = "Url")
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// Private key input field
OutlinedTextField(
value = cert,
onValueChange = { cert = it },
label = { Text("Cert") },
singleLine = false,
maxLines = 40,
minLines = 20,
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 8.sp
)
)
Spacer(modifier = Modifier.height(24.dp))
// Save Button
Button(
onClick = {
meScreenViewModel.updateServer(url, cert, context)
},
modifier = Modifier.fillMaxWidth()
) {
Text("Save")
}
}
}
}
}
}

View File

@@ -32,6 +32,8 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val URL_KEY = stringPreferencesKey("url")
private val CERT_KEY = stringPreferencesKey("cert")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
@@ -41,6 +43,14 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
var _init = false
@Composable
@@ -60,13 +70,16 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
viewModelScope.launch {
val u = userNameFlow.first()
val p = privateKeyFlow.first()
val ur = urlFlow.first()
val c = certFlow.first()
if(u=="" || p=="") return@launch
if(u=="" || p=="" || ur=="" || c=="") return@launch
try{
ApiClient.apply(ur, c)
if (MediaManager.token == "null")
MediaManager.token = AuthManager.fetchToken(
ApiClient.base,
u,
p
)!!
@@ -74,6 +87,7 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
Global.loggedIn = true
}catch(e: Exception)
{
Global.loggedIn = false
print(e.message)
}
}

View File

@@ -24,23 +24,70 @@ import kotlinx.coroutines.launch
class MeScreenViewModel(application: Application) : AndroidViewModel(application) {
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val URL_KEY = stringPreferencesKey("url")
private val CERT_KEY = stringPreferencesKey("cert")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
val username = mutableStateOf("");
val privateKey = mutableStateOf("")
val url = mutableStateOf("");
val cert = mutableStateOf("")
init {
viewModelScope.launch {
username.value = userNameFlow.first()
privateKey.value = if (privateKeyFlow.first() == "") "" else "******"
url.value = urlFlow.first()
cert.value = certFlow.first()
}
}
fun updateServer(u: String, c: String, context: Context)
{
viewModelScope.launch {
dataStore.edit { preferences ->
preferences[URL_KEY] = u
preferences[CERT_KEY] = c
}
Global.loggedIn = false
val us = userNameFlow.first()
val u = urlFlow.first()
val c = certFlow.first()
val p = privateKeyFlow.first()
if (u == "" || c == "" || p == "") return@launch
try {
ApiClient.apply(u, c)
MediaManager.token = AuthManager.fetchToken(
us,
p
)!!
Global.loggedIn = true
Toast.makeText(context, "Server Updated", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
print(e.message)
Toast.makeText(context, "Invalid Account or Server Information", Toast.LENGTH_SHORT).show()
}
}
}
@@ -62,7 +109,6 @@ class MeScreenViewModel(application: Application) : AndroidViewModel(application
try {
MediaManager.token = AuthManager.fetchToken(
ApiClient.base,
u,
p
)!!