package com.acitelight.aether.service import android.content.Context import android.util.Log import androidx.core.net.toUri import com.acitelight.aether.AetherApp import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import okhttp3.ConnectionSpec import okhttp3.EventListener 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.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.security.KeyStore import java.security.cert.CertificateException 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 { fun getBase(): String{ return replaceAbyssProtocol(base) } private var base: String = "" var domain: String = "" var cert: String = "" private val json = Json { ignoreUnknownKeys = true } fun replaceAbyssProtocol(uri: String): String { return uri.replaceFirst("^abyss://".toRegex(), "https://") } private val dnsEventListener = object : EventListener() { override fun dnsEnd(call: okhttp3.Call, domainName: String, inetAddressList: List) { super.dnsEnd(call, domainName, inetAddressList) val ipAddresses = inetAddressList.joinToString(", ") { it.hostAddress ?: "" } Log.d("OkHttp_DNS", "Domain '$domainName' resolved to IPs: [$ipAddresses]") } } fun loadCertificateFromString(pemString: String): X509Certificate { val certificateFactory = CertificateFactory.getInstance("X.509") val decodedPem = pemString .replace("-----BEGIN CERTIFICATE-----", "") .replace("-----END CERTIFICATE-----", "") .replace("\\s+".toRegex(), "") 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 defaultTmFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ).apply { init(null as KeyStore?) } val defaultTm = defaultTmFactory.trustManagers .first { it is X509TrustManager } as X509TrustManager val customTm: X509TrustManager? = trustedCert?.let { val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null, null) setCertificateEntry("ca", it) } val tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ).apply { init(keyStore) } tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager } val combinedTm = object : X509TrustManager { override fun getAcceptedIssuers(): Array { return (defaultTm.acceptedIssuers + (customTm?.acceptedIssuers ?: emptyArray())) } override fun checkClientTrusted(chain: Array, authType: String) { var passed = false try { defaultTm.checkClientTrusted(chain, authType) passed = true } catch (_: CertificateException) { } if (!passed && customTm != null) { customTm.checkClientTrusted(chain, authType) passed = true } if (!passed) throw CertificateException("Untrusted client certificate chain") } override fun checkServerTrusted(chain: Array, authType: String) { var passed = false try { defaultTm.checkServerTrusted(chain, authType) passed = true } catch (_: CertificateException) { } if (!passed && customTm != null) { customTm.checkServerTrusted(chain, authType) passed = true } if (!passed) throw CertificateException("Untrusted server certificate chain") } } val sslContext = SSLContext.getInstance("TLS").apply { init(null, arrayOf(combinedTm), null) } return if (base.startsWith("abyss://")) OkHttpClient.Builder() .connectionSpecs( listOf( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS ) ) .proxy( Proxy( Proxy.Type.HTTP, InetSocketAddress("::1", 4095) ) ) .sslSocketFactory(sslContext.socketFactory, combinedTm) .build() else OkHttpClient.Builder() .connectionSpecs( listOf( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS ) ) .sslSocketFactory(sslContext.socketFactory, combinedTm) .build() } catch (e: Exception) { throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e) } } fun createOkHttp(): OkHttpClient { return if (cert == "") if (base.startsWith("abyss://")) OkHttpClient .Builder() .proxy( Proxy( Proxy.Type.HTTP, InetSocketAddress("::1", 4095) ) ) .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .eventListener(dnsEventListener) .build() else OkHttpClient .Builder() .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .eventListener(dnsEventListener) .build() else createOkHttpClientWithDynamicCert(loadCertificateFromString(cert)) } private fun createRetrofit(): Retrofit { val okHttpClient = createOkHttp() val b = replaceAbyssProtocol(base) return Retrofit.Builder() .baseUrl(b) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() } var api: ApiInterface? = null suspend fun apply(context: Context, urls: String, crt: String): String? { try { val urlList = urls.split(";").map { it.trim() } var selectedUrl: String? = null for (url in urlList) { val host = url.toUri().host if (host != null && pingHost(host)) { selectedUrl = url break } } if (selectedUrl == null) { throw Exception("No reachable URL found") } domain = selectedUrl.toHttpUrlOrNull()?.host ?: "" cert = crt base = selectedUrl withContext(Dispatchers.IO) { (context as AetherApp).abyssService?.proxy?.config(ApiClient.getBase().toUri().host!!, 4096) } api = createRetrofit().create(ApiInterface::class.java) Log.i("Delay Analyze", "Start Abyss Hello") val h = api!!.hello() Log.i("Delay Analyze", "Abyss Hello: ${h.string()}") return base } catch (e: Exception) { api = null base = "" domain = "" cert = "" return null } } private suspend fun pingHost(host: String): Boolean = withContext(Dispatchers.IO) { return@withContext try { val address = InetAddress.getByName(host) address.isReachable(200) } catch (e: Exception) { false } } }