[feat] Abyss 'HELLY' encryption protocol 😈

This commit is contained in:
acite
2025-09-13 03:15:08 +08:00
parent 10f316cb48
commit d28804178e
12 changed files with 665 additions and 28 deletions

View File

@@ -0,0 +1,40 @@
package com.acitelight.aether
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import com.acitelight.aether.service.AbyssTunnelProxy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class AbyssService : Service() {
private val binder = AbyssServiceBinder()
private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val serviceScope = CoroutineScope(Dispatchers.IO + Job())
var proxy = AbyssTunnelProxy()
inner class AbyssServiceBinder : Binder() {
fun getService(): AbyssService = this@AbyssService
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun onCreate() {
super.onCreate()
serviceScope.launch {
_isInitialized.update { true }
proxy.start()
}
}
}

View File

@@ -1,17 +1,51 @@
package com.acitelight.aether
import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Singleton
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "configure")
@HiltAndroidApp
class AetherApp : Application() {
var abyssService: AbyssService? = null
var isServiceBound = false
private set
val isServiceInitialized: StateFlow<Boolean>?
get() = abyssService?.isInitialized
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as AbyssService.AbyssServiceBinder
abyssService = binder.getService()
isServiceBound = true
}
override fun onServiceDisconnected(name: ComponentName?) {
isServiceBound = false
abyssService = null
}
}
override fun onCreate() {
super.onCreate()
val intent = Intent(this, AbyssService::class.java)
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
}
}

View File

@@ -1,6 +1,7 @@
package com.acitelight.aether
import android.app.Activity
import android.content.Intent
import androidx.compose.material.icons.Icons
import android.graphics.drawable.Icon
import android.net.http.SslCertificate.saveState
@@ -39,6 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavType
@@ -57,6 +59,9 @@ import com.acitelight.aether.view.VideoPlayer
import com.acitelight.aether.view.VideoScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -66,6 +71,21 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = application as AetherApp
lifecycleScope.launch {
app.isServiceInitialized?.filter { it }?.first()
val intent = Intent(this@MainActivity, MainScreenActivity::class.java)
startActivity(intent)
finish()
}
}
}
@AndroidEntryPoint
class MainScreenActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}

View File

@@ -10,7 +10,7 @@ class Comic(
{
fun getPage(pageNumber: Int): String
{
return "${ApiClient.base}api/image/$id/${comic.list[pageNumber]}?token=$token"
return "${ApiClient.getBase()}api/image/$id/${comic.list[pageNumber]}?token=$token"
}
fun getPage(pageName: String): String?

View File

@@ -11,18 +11,18 @@ class Video constructor(
){
fun getCover(): String
{
return "${ApiClient.base}api/video/$klass/$id/cover?token=$token"
return "${ApiClient.getBase()}api/video/$klass/$id/cover?token=$token"
}
fun getVideo(): String
{
return "${ApiClient.base}api/video/$klass/$id/av?token=$token"
return "${ApiClient.getBase()}api/video/$klass/$id/av?token=$token"
}
fun getGallery(): List<KeyImage>
{
return video.gallery.map{
KeyImage(url = "${ApiClient.base}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it")
KeyImage(url = "${ApiClient.getBase()}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it")
}
}

View File

@@ -0,0 +1,357 @@
package com.acitelight.aether.service
import kotlinx.coroutines.*
import java.io.InputStream
import java.io.OutputStream
import java.net.Socket
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.ArrayDeque
import org.bouncycastle.math.ec.rfc7748.X25519
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.params.HKDFParameters
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.AEADParameters
import org.bouncycastle.crypto.modes.ChaCha20Poly1305
import java.io.EOFException
import java.util.concurrent.atomic.AtomicLong
class AbyssStream private constructor(
private val socket: Socket,
private val input: InputStream,
private val output: OutputStream,
private val aeadKey: ByteArray,
private val sendSalt: ByteArray,
private val recvSalt: ByteArray
) {
companion object {
private const val PUBLIC_KEY_LEN = 32
private const val AEAD_KEY_LEN = 32
private const val NONCE_SALT_LEN = 4
private const val AEAD_TAG_LEN = 16
private const val NONCE_LEN = 12
private const val MAX_PLAINTEXT_FRAME = 64 * 1024
private val secureRandom = SecureRandom()
/**
* Create and perform handshake on an already-connected socket.
* If privateKeyRaw is provided, it must be 32 bytes.
*/
suspend fun create(socket: Socket, privateKeyRaw: ByteArray? = null): AbyssStream = withContext(Dispatchers.IO) {
if (!socket.isConnected) throw IllegalArgumentException("socket is not connected")
val inStream = socket.getInputStream()
val outStream = socket.getOutputStream()
// 1) keypair (raw)
val localPriv = ByteArray(PUBLIC_KEY_LEN)
if (privateKeyRaw != null) {
if (privateKeyRaw.size != PUBLIC_KEY_LEN) {
throw IllegalArgumentException("privateKeyRaw must be $PUBLIC_KEY_LEN bytes")
}
System.arraycopy(privateKeyRaw, 0, localPriv, 0, PUBLIC_KEY_LEN)
} else {
X25519.generatePrivateKey(secureRandom, localPriv)
}
val localPub = ByteArray(PUBLIC_KEY_LEN)
X25519.scalarMultBase(localPriv, 0, localPub, 0)
// 2) exchange raw public keys (exact 32 bytes each) using blocking IO
writeExact(outStream, localPub, 0, PUBLIC_KEY_LEN)
val remotePub = ByteArray(PUBLIC_KEY_LEN)
readExact(inStream, remotePub, 0, PUBLIC_KEY_LEN)
// 3) compute shared secret: X25519.scalarMult(private, remotePublic)
val shared = ByteArray(PUBLIC_KEY_LEN)
X25519.scalarMult(localPriv, 0, remotePub, 0, shared, 0)
// 4) HKDF-SHA256 -> AEAD key + saltA + saltB
val hkdf = HKDFBytesGenerator(SHA256Digest())
// AEAD key
hkdf.init(HKDFParameters(shared, null, "Abyss-AEAD-Key".toByteArray(Charsets.US_ASCII)))
val aeadKey = ByteArray(AEAD_KEY_LEN)
hkdf.generateBytes(aeadKey, 0, AEAD_KEY_LEN)
// salt A
hkdf.init(HKDFParameters(shared, null, "Abyss-Nonce-Salt-A".toByteArray(Charsets.US_ASCII)))
val saltA = ByteArray(NONCE_SALT_LEN)
hkdf.generateBytes(saltA, 0, NONCE_SALT_LEN)
// salt B
hkdf.init(HKDFParameters(shared, null, "Abyss-Nonce-Salt-B".toByteArray(Charsets.US_ASCII)))
val saltB = ByteArray(NONCE_SALT_LEN)
hkdf.generateBytes(saltB, 0, NONCE_SALT_LEN)
// Deterministic assignment by lexicographic comparison
val cmp = lexicographicCompare(localPub, remotePub)
val sendSalt: ByteArray
val recvSalt: ByteArray
if (cmp < 0) {
sendSalt = saltA
recvSalt = saltB
} else if (cmp > 0) {
sendSalt = saltB
recvSalt = saltA
} else {
// extremely unlikely
sendSalt = saltA
recvSalt = saltB
}
// zero sensitive buffers
localPriv.fill(0)
localPub.fill(0)
remotePub.fill(0)
shared.fill(0)
// keep aeadKey, sendSalt, recvSalt
return@withContext AbyssStream(socket, inStream, outStream, aeadKey, sendSalt, recvSalt)
}
private fun lexicographicCompare(a: ByteArray, b: ByteArray): Int {
val min = kotlin.math.min(a.size, b.size)
for (i in 0 until min) {
val av = a[i].toInt() and 0xff
val bv = b[i].toInt() and 0xff
if (av < bv) return -1
if (av > bv) return 1
}
if (a.size < b.size) return -1
if (a.size > b.size) return 1
return 0
}
private fun readExact(input: InputStream, buffer: ByteArray, offset: Int, count: Int) {
var read = 0
while (read < count) {
val n = input.read(buffer, offset + read, count - read)
if (n == -1) {
if (read == 0) throw EOFException("Remote closed connection while reading")
else throw EOFException("Remote closed connection unexpectedly during read")
}
read += n
}
}
private fun writeExact(output: OutputStream, buffer: ByteArray, offset: Int, count: Int) {
output.write(buffer, offset, count)
output.flush()
}
}
// internal state
private val sendCounter = AtomicLong(0L)
private val recvCounter = AtomicLong(0L)
private val sendLock = Any()
private val aeadLock = Any()
// leftover read queue
private val leftoverQueue = ArrayDeque<ByteArray>()
private var currentLeftover: ByteArray? = null
private var currentLeftoverOffset = 0
@Volatile
private var closed = false
// ---- high-level read/write APIs (suspendable) ----
suspend fun read(buffer: ByteArray, offset: Int, count: Int): Int = withContext(Dispatchers.IO) {
if (closed) throw IllegalStateException("AbyssStream closed")
if (buffer.size < offset + count) throw IndexOutOfBoundsException()
// serve leftover first
if (ensureCurrentLeftover()) {
val seg = currentLeftover!!
val avail = seg.size - currentLeftoverOffset
val toCopy = kotlin.math.min(avail, count)
System.arraycopy(seg, currentLeftoverOffset, buffer, offset, toCopy)
currentLeftoverOffset += toCopy
if (currentLeftoverOffset >= seg.size) {
currentLeftover = null
currentLeftoverOffset = 0
}
return@withContext toCopy
}
// read one frame and decrypt
val plaintext = readOneFrameAndDecrypt()
if (plaintext == null || plaintext.isEmpty()) {
// EOF
return@withContext 0
}
return@withContext if (plaintext.size <= count) {
System.arraycopy(plaintext, 0, buffer, offset, plaintext.size)
plaintext.size
} else {
System.arraycopy(plaintext, 0, buffer, offset, count)
val leftoverLen = plaintext.size - count
val leftover = ByteArray(leftoverLen)
System.arraycopy(plaintext, count, leftover, 0, leftoverLen)
synchronized(leftoverQueue) { leftoverQueue.addLast(leftover) }
count
}
}
private fun ensureCurrentLeftover(): Boolean {
if (currentLeftover != null && currentLeftoverOffset < currentLeftover!!.size) return true
synchronized(leftoverQueue) {
val next = leftoverQueue.pollFirst()
if (next != null) {
currentLeftover = next
currentLeftoverOffset = 0
return true
}
}
return false
}
private fun readOneFrameAndDecrypt(): ByteArray? {
// read 4-byte header
val header = ByteArray(4)
try {
readExact(input, header, 0, 4)
} catch (e: EOFException) {
return null
}
val payloadLen = ByteBuffer.wrap(header).int and 0xffffffff.toInt()
if (payloadLen > MAX_PLAINTEXT_FRAME + AEAD_TAG_LEN) throw IllegalStateException("payload too big")
if (payloadLen < AEAD_TAG_LEN) throw IllegalStateException("payload too small")
val payload = ByteArray(payloadLen)
readExact(input, payload, 0, payloadLen)
val ciphertextLen = payloadLen - AEAD_TAG_LEN
val ciphertext = ByteArray(ciphertextLen)
val tag = ByteArray(AEAD_TAG_LEN)
if (ciphertextLen > 0) System.arraycopy(payload, 0, ciphertext, 0, ciphertextLen)
System.arraycopy(payload, ciphertextLen, tag, 0, AEAD_TAG_LEN)
val remoteCounterValue = recvCounter.getAndIncrement()
val nonce = ByteArray(NONCE_LEN)
System.arraycopy(recvSalt, 0, nonce, 0, NONCE_SALT_LEN)
// write 8-byte big-endian counter at nonce[4..11]
val bb = ByteBuffer.wrap(nonce, NONCE_SALT_LEN, 8)
bb.putLong(remoteCounterValue)
val plaintext = try {
aeadDecrypt(nonce, ciphertext, tag)
} catch (ex: Exception) {
close()
throw SecurityException("AEAD authentication failed; connection closed.", ex)
} finally {
nonce.fill(0)
payload.fill(0)
ciphertext.fill(0)
tag.fill(0)
}
return plaintext
}
suspend fun write(buffer: ByteArray, offset: Int, count: Int) = withContext(Dispatchers.IO) {
if (closed) throw IllegalStateException("AbyssStream closed")
if (buffer.size < offset + count) throw IndexOutOfBoundsException()
var remaining = count
var idx = offset
while (remaining > 0) {
val chunk = kotlin.math.min(remaining, MAX_PLAINTEXT_FRAME)
val plaintext = buffer.copyOfRange(idx, idx + chunk)
sendPlaintextChunk(plaintext)
idx += chunk
remaining -= chunk
}
}
private fun sendPlaintextChunk(plaintext: ByteArray) {
if (closed) throw IllegalStateException("AbyssStream closed")
val ciphertextAndTag: ByteArray
val nonce = ByteArray(NONCE_LEN)
val counterValue: Long
synchronized(sendLock) {
counterValue = sendCounter.getAndIncrement()
}
System.arraycopy(sendSalt, 0, nonce, 0, NONCE_SALT_LEN)
val bb = ByteBuffer.wrap(nonce, NONCE_SALT_LEN, 8)
bb.putLong(counterValue)
try {
ciphertextAndTag = aeadEncrypt(nonce, plaintext)
} finally {
nonce.fill(0)
}
val payloadLen = ciphertextAndTag.size
val header = ByteBuffer.allocate(4).putInt(payloadLen).array()
try {
synchronized(output) {
output.write(header)
if (payloadLen > 0) output.write(ciphertextAndTag)
output.flush()
}
} finally {
// clear sensitive
ciphertextAndTag.fill(0)
plaintext.fill(0)
}
}
// ---- AEAD helpers using BouncyCastle lightweight API ----
// ChaCha20-Poly1305 with 12-byte nonce. BouncyCastle ChaCha20Poly1305 produces ciphertext+tag.
private fun aeadEncrypt(nonce: ByteArray, plaintext: ByteArray): ByteArray {
synchronized(aeadLock) {
val cipher = ChaCha20Poly1305()
val params = AEADParameters(KeyParameter(aeadKey), AEAD_TAG_LEN * 8, nonce, null)
cipher.init(true, params)
val outBuf = ByteArray(cipher.getOutputSize(plaintext.size))
var len = cipher.processBytes(plaintext, 0, plaintext.size, outBuf, 0)
len += cipher.doFinal(outBuf, len)
if (len != outBuf.size) return outBuf.copyOf(len)
return outBuf
}
}
private fun aeadDecrypt(nonce: ByteArray, ciphertext: ByteArray, tag: ByteArray): ByteArray {
synchronized(aeadLock) {
val cipher = ChaCha20Poly1305()
val params = AEADParameters(KeyParameter(aeadKey), AEAD_TAG_LEN * 8, nonce, null)
cipher.init(false, params)
// input is ciphertext||tag
val input = ByteArray(ciphertext.size + tag.size)
if (ciphertext.isNotEmpty()) System.arraycopy(ciphertext, 0, input, 0, ciphertext.size)
System.arraycopy(tag, 0, input, ciphertext.size, tag.size)
val outBuf = ByteArray(cipher.getOutputSize(input.size))
var len = cipher.processBytes(input, 0, input.size, outBuf, 0)
try {
len += cipher.doFinal(outBuf, len)
} catch (ex: Exception) {
// authentication failure or other
throw ex
}
return if (len != outBuf.size) outBuf.copyOf(len) else outBuf
}
}
// ---- utility / lifecycle ----
fun close() {
if (!closed) {
closed = true
try { socket.close() } catch (_: Exception) {}
// clear secrets
aeadKey.fill(0)
sendSalt.fill(0)
recvSalt.fill(0)
synchronized(leftoverQueue) {
leftoverQueue.forEach { it.fill(0) }
leftoverQueue.clear()
}
currentLeftover = null
}
}
}

View File

@@ -0,0 +1,116 @@
package com.acitelight.aether.service
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.select
import java.io.InputStream
import java.io.OutputStream
import java.net.InetAddress
import java.net.ServerSocket
import java.net.Socket
import kotlin.coroutines.CoroutineContext
class AbyssTunnelProxy(
private val coroutineContext: CoroutineContext = Dispatchers.IO
) {
private var serverHost: String = ""
private var serverPort: Int = 0
fun config(host: String, port: Int)
{
serverHost = host
serverPort = port
}
private val listenAddress = InetAddress.getLoopbackAddress()
private val listenPort = 4095
private var serverSocket: ServerSocket? = null
private val scope = CoroutineScope(SupervisorJob() + coroutineContext)
fun start() {
serverSocket = ServerSocket(listenPort, 50, listenAddress)
// accept loop
scope.launch {
val srv = serverSocket ?: return@launch
try {
while (true) {
val client = srv.accept()
if(serverHost.isEmpty())
continue
launch {
try { handleLocalConnection(client) }
catch (ex: Exception) { /* ignore */ }
}
}
} catch (ex: Exception) {
println(ex.message)
// server stopped or fatal error
} finally {
stop()
}
}
}
fun stop() {
try { serverSocket?.close() } catch (_: Exception) {}
scope.cancel()
}
private suspend fun handleLocalConnection(localSocket: Socket) = withContext(coroutineContext) {
val localIn = localSocket.getInputStream()
val localOut = localSocket.getOutputStream()
var abyssSocket: Socket? = null
var abyssStream: AbyssStream? = null
try {
abyssSocket = Socket(serverHost, serverPort)
abyssStream = AbyssStream.create(abyssSocket)
// concurrently copy in both directions
val job1 = launch { copyExactSuspend(localIn, abyssStream) } // local -> abyss
val job2 = launch { copyFromAbyssToLocal(abyssStream, localOut) } // abyss -> local
// wait for either direction to finish
select<Unit> {
job1.onJoin { /* completed */ }
job2.onJoin { /* completed */ }
}
// cancel other
job1.cancel()
job2.cancel()
} catch (ex: Exception)
{
println(ex.message)
// log or ignore; we close sockets below
} finally {
try { localSocket.close() } catch (_: Exception) {}
try { abyssStream?.close() } catch (_: Exception) {}
try { abyssSocket?.close() } catch (_: Exception) {}
}
return@withContext
}
// Copy from local InputStream into AbyssStream.write in frames.
private suspend fun copyExactSuspend(localIn: InputStream, abyss: AbyssStream) = withContext(coroutineContext) {
val buffer = ByteArray(16 * 1024)
while (true) {
val read = localIn.read(buffer)
if (read <= 0)
break
abyss.write(buffer, 0, read)
}
}
// Copy from AbyssStream (read frames/decrypt) to local OutputStream
private suspend fun copyFromAbyssToLocal(abyss: AbyssStream, localOut: OutputStream) = withContext(coroutineContext) {
val buffer = ByteArray(16 * 1024)
while (true) {
val n = abyss.read(buffer, 0, buffer.size)
if (n <= 0)
break
localOut.write(buffer, 0, n)
localOut.flush()
}
}
}

View File

@@ -1,16 +1,25 @@
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
@@ -18,19 +27,23 @@ import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import okhttp3.EventListener
import java.net.InetAddress
import android.util.Log
import okhttp3.ConnectionSpec
object ApiClient {
var base: String = ""
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<InetAddress>) {
super.dnsEnd(call, domainName, inetAddressList)
@@ -112,10 +125,32 @@ object ApiClient {
init(null, arrayOf(combinedTm), null)
}
return OkHttpClient.Builder()
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.sslSocketFactory(sslContext.socketFactory, combinedTm)
.build()
return if (base.startsWith("abyss://"))
OkHttpClient.Builder()
.connectionSpecs(
listOf(
ConnectionSpec.MODERN_TLS,
ConnectionSpec.COMPATIBLE_TLS
)
)
.proxy(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress("127.0.0.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)
@@ -124,11 +159,24 @@ object ApiClient {
fun createOkHttp(): OkHttpClient {
return if (cert == "")
OkHttpClient
.Builder()
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.eventListener(dnsEventListener)
.build()
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))
@@ -136,9 +184,10 @@ object ApiClient {
private fun createRetrofit(): Retrofit {
val okHttpClient = createOkHttp()
val b = replaceAbyssProtocol(base)
return Retrofit.Builder()
.baseUrl(base)
.baseUrl(b)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
@@ -148,13 +197,13 @@ object ApiClient {
var api: ApiInterface? = null
suspend fun apply(urls: String, crt: String): String? {
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.toHttpUrlOrNull()?.host
val host = url.toUri().host
if (host != null && pingHost(host)) {
selectedUrl = url
break
@@ -168,6 +217,12 @@ object ApiClient {
domain = selectedUrl.toHttpUrlOrNull()?.host ?: ""
cert = crt
base = selectedUrl
withContext(Dispatchers.IO)
{
(context as AetherApp).abyssService?.proxy?.config(base.toUri().host!!, 4096)
}
api = createRetrofit().create(ApiInterface::class.java)
return base
} catch (e: Exception) {

View File

@@ -1,6 +1,7 @@
package com.acitelight.aether.viewModel
import android.app.Application
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -34,11 +35,13 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
import com.acitelight.aether.service.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
private val settingsDataStoreManager: SettingsDataStoreManager
private val settingsDataStoreManager: SettingsDataStoreManager,
@ApplicationContext private val context: Context
) : ViewModel()
{
var _init = false
@@ -73,7 +76,7 @@ class HomeScreenViewModel @Inject constructor(
if(u=="" || p=="" || ur=="") return@launch
try{
val usedUrl = ApiClient.apply(ur, if(uss.first()) c else "")
val usedUrl = ApiClient.apply(context, ur, if(uss.first()) c else "")
if (MediaManager.token == "null")
MediaManager.token = AuthManager.fetchToken(

View File

@@ -24,10 +24,12 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
import com.acitelight.aether.service.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@HiltViewModel
class MeScreenViewModel @Inject constructor(
private val settingsDataStoreManager: SettingsDataStoreManager
private val settingsDataStoreManager: SettingsDataStoreManager,
@ApplicationContext private val context: Context
) : ViewModel() {
val username = mutableStateOf("");
@@ -66,7 +68,7 @@ class MeScreenViewModel @Inject constructor(
if (u == "" || p == "" || us == "") return@launch
try {
val usedUrl = ApiClient.apply(u, if(uss.first()) c else "")
val usedUrl = ApiClient.apply(context, u, if(uss.first()) c else "")
MediaManager.token = AuthManager.fetchToken(
us,
p