浏览代码

Replace okhttp/conscrypt/crypto with jni implementation

世界 3 年之前
父节点
当前提交
b2a815dc2b

+ 1 - 6
app/build.gradle.kts

@@ -57,20 +57,15 @@ dependencies {
     implementation("cn.hutool:hutool-core:$hutoolVersion")
     implementation("cn.hutool:hutool-cache:$hutoolVersion")
     implementation("cn.hutool:hutool-json:$hutoolVersion")
-    implementation("cn.hutool:hutool-crypto:$hutoolVersion")
     implementation("com.google.code.gson:gson:2.8.9")
     implementation("com.google.zxing:core:3.4.1")
 
-    implementation(platform("com.squareup.okhttp3:okhttp-bom:5.0.0-alpha.3"))
-    implementation("com.squareup.okhttp3:okhttp")
-    implementation("com.squareup.okhttp3:okhttp-dnsoverhttps")
-
+    implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3")
     implementation("org.yaml:snakeyaml:1.30")
     implementation("com.github.daniel-stoneuk:material-about-library:3.2.0-rc01")
     implementation("com.mikepenz:aboutlibraries:8.9.4")
     implementation("com.jakewharton:process-phoenix:2.1.2")
     implementation("com.esotericsoftware:kryo:5.2.1")
-    implementation("org.conscrypt:conscrypt-android:2.5.2")
     implementation("com.google.guava:guava:31.0.1-android")
     implementation("com.journeyapps:zxing-android-embedded:4.3.0")
     implementation("org.ini4j:ini4j:0.5.4")

+ 4 - 5
app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt

@@ -53,12 +53,13 @@ import io.nekohasekai.sagernet.ktx.app
 import io.nekohasekai.sagernet.ktx.checkMT
 import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
 import io.nekohasekai.sagernet.ui.MainActivity
-import io.nekohasekai.sagernet.utils.*
+import io.nekohasekai.sagernet.utils.CrashHandler
+import io.nekohasekai.sagernet.utils.DeviceStorageApp
+import io.nekohasekai.sagernet.utils.PackageCache
+import io.nekohasekai.sagernet.utils.Theme
 import kotlinx.coroutines.DEBUG_PROPERTY_NAME
 import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
 import libcore.Libcore
-import org.conscrypt.Conscrypt
-import java.security.Security
 import androidx.work.Configuration as WorkConfiguration
 
 class SagerNet : Application(),
@@ -98,8 +99,6 @@ class SagerNet : Application(),
         Theme.apply(this)
         Theme.applyNightTheme()
 
-        Security.insertProviderAt(Conscrypt.newProvider(), 1)
-
         if (BuildConfig.DEBUG) StrictMode.setVmPolicy(
             StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()

+ 3 - 2
app/src/main/java/io/nekohasekai/sagernet/bg/proto/V2RayInstance.kt

@@ -57,7 +57,6 @@ import io.nekohasekai.sagernet.ktx.*
 import io.nekohasekai.sagernet.plugin.PluginManager
 import kotlinx.coroutines.*
 import libcore.V2RayInstance
-import okhttp3.internal.closeQuietly
 import java.io.File
 import java.util.concurrent.atomic.AtomicBoolean
 
@@ -388,7 +387,9 @@ abstract class V2RayInstance(
     @Suppress("EXPERIMENTAL_API_USAGE")
     override fun close() {
         for (instance in externalInstances.values) {
-            instance.closeQuietly()
+            runCatching {
+                instance.close()
+            }
         }
 
         cacheFiles.removeAll { it.delete(); true }

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt

@@ -163,7 +163,7 @@ fun buildV2RayConfig(
     val trafficSniffing = DataStore.trafficSniffing
     val indexMap = ArrayList<IndexEntity>()
     var requireWs = false
-    val requireHttp = !forTest && (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M || DataStore.requireHttp)
+    val requireHttp = !forTest && DataStore.requireHttp
     val requireTransproxy = if (forTest) false else DataStore.requireTransproxy
     val ipv6Mode = if (forTest) IPv6Mode.ENABLE else DataStore.ipv6Mode
     val resolveDestination = DataStore.resolveDestination

+ 29 - 16
app/src/main/java/io/nekohasekai/sagernet/group/GroupUpdater.kt

@@ -38,10 +38,7 @@ import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
 import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean
 import io.nekohasekai.sagernet.ktx.*
 import kotlinx.coroutines.*
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import okhttp3.OkHttpClient
-import okhttp3.dnsoverhttps.DnsOverHttps
+import libcore.Libcore
 import java.net.Inet4Address
 import java.net.InetAddress
 import java.util.*
@@ -54,7 +51,6 @@ abstract class GroupUpdater {
         proxyGroup: ProxyGroup,
         subscription: SubscriptionBean,
         userInterface: GroupManager.Interface?,
-        httpClient: OkHttpClient,
         byUser: Boolean
     )
 
@@ -65,7 +61,7 @@ abstract class GroupUpdater {
     }
 
     protected suspend fun forceResolve(
-        okHttpClient: OkHttpClient, profiles: List<AbstractBean>, groupId: Long?
+        profiles: List<AbstractBean>, groupId: Long?
     ) {
         val connected = DataStore.startedProfile > 0
 
@@ -88,18 +84,21 @@ abstract class GroupUpdater {
             }
         }
 
-        val dohHttpUrl = dohUrl?.toHttpUrlOrNull() ?: (if (connected) {
+        val dohHttpUrl = dohUrl ?: if (connected) {
             "https://dns.google/dns-query"
         } else {
             "https://doh.pub/dns-query"
-        }).toHttpUrl()
+        }
+
+        val client = Libcore.newHttpClient().apply {
+            modernTLS()
+            keepAlive()
+            trySocks5(DataStore.socksPort)
+        }
 
         Logs.d("Using doh url $dohHttpUrl")
 
         val ipv6Mode = DataStore.ipv6Mode
-        val dohClient = DnsOverHttps.Builder().client(okHttpClient).url(dohHttpUrl).apply {
-            if (ipv6Mode == IPv6Mode.DISABLE) includeIPv6(false)
-        }.build()
         val lookupPool = newFixedThreadPoolContext(5, "DNS Lookup")
         val lookupJobs = mutableListOf<Job>()
         val progress = Progress(profiles.size)
@@ -120,11 +119,26 @@ abstract class GroupUpdater {
 
             lookupJobs.add(GlobalScope.launch(lookupPool) {
                 try {
-                    val results = dohClient.lookup(profile.serverAddress)
+                    val message = Libcore.encodeDomainNameSystemQuery(
+                        1, profile.serverAddress, ipv6Mode
+                    )
+                    val response = client.newRequest().apply {
+                        setMethod("POST")
+                        setURL(dohHttpUrl)
+                        setContent(message)
+                        setHeader("Accept", "application/dns-message")
+                        setHeader("Content-Type", "application/dns-message")
+                    }.execute()
+
+                    val results = Libcore.decodeContentDomainNameSystemResponse(response.content)
+                        .trimStart()
+                        .split(" ")
+                        .map { InetAddress.getByName(it) }
+
                     if (results.isEmpty()) error("empty response")
                     rewriteAddress(profile, results, ipv6First)
                 } catch (e: Exception) {
-                    Logs.d("Lookup ${profile.serverAddress} failed: ${e.readableMessage}")
+                    Logs.d("Lookup ${profile.serverAddress} failed: ${e.readableMessage}",e)
                 }
                 if (groupId != null) {
                     progress.progress++
@@ -133,6 +147,7 @@ abstract class GroupUpdater {
             })
         }
 
+        client.close()
         lookupJobs.joinAll()
         lookupPool.close()
     }
@@ -188,8 +203,6 @@ abstract class GroupUpdater {
 
                 val subscription = proxyGroup.subscription!!
                 val connected = DataStore.startedProfile > 0
-
-                val httpClient = createProxyClient()
                 val userInterface = GroupManager.userInterface
 
                 if (userInterface != null) {
@@ -207,7 +220,7 @@ abstract class GroupUpdater {
                         SubscriptionType.OOCv1 -> OpenOnlineConfigUpdater
                         SubscriptionType.SIP008 -> SIP008Updater
                         else -> error("wtf")
-                    }.doUpdate(proxyGroup, subscription, userInterface, httpClient, byUser)
+                    }.doUpdate(proxyGroup, subscription, userInterface, byUser)
                     true
                 } catch (e: Throwable) {
                     Logs.w(e)

+ 12 - 45
app/src/main/java/io/nekohasekai/sagernet/group/OpenOnlineConfigUpdater.kt

@@ -19,10 +19,7 @@
 
 package io.nekohasekai.sagernet.group
 
-import android.annotation.SuppressLint
-import cn.hutool.core.net.DefaultTrustManager
 import cn.hutool.core.util.CharUtil
-import cn.hutool.crypto.digest.DigestUtil
 import cn.hutool.json.JSONObject
 import com.github.shadowsocks.plugin.PluginConfiguration
 import com.github.shadowsocks.plugin.PluginOptions
@@ -36,23 +33,16 @@ import io.nekohasekai.sagernet.ktx.Logs
 import io.nekohasekai.sagernet.ktx.USER_AGENT_ORIGIN
 import io.nekohasekai.sagernet.ktx.app
 import io.nekohasekai.sagernet.ktx.applyDefaultValues
-import okhttp3.*
+import libcore.Libcore
+import okhttp3.HttpUrl
 import okhttp3.HttpUrl.Companion.toHttpUrl
-import java.security.cert.CertificateException
-import java.security.cert.X509Certificate
-import javax.net.ssl.SSLSocketFactory
 
 object OpenOnlineConfigUpdater : GroupUpdater() {
 
-    val oocConnSpec = ConnectionSpec.Builder(ConnectionSpec.RESTRICTED_TLS)
-        .tlsVersions(TlsVersion.TLS_1_3)
-        .build()
-
     override suspend fun doUpdate(
         proxyGroup: ProxyGroup,
         subscription: SubscriptionBean,
         userInterface: GroupManager.Interface?,
-        httpClient: OkHttpClient,
         byUser: Boolean
     ) {
         val apiToken: JSONObject
@@ -108,24 +98,16 @@ object OpenOnlineConfigUpdater : GroupUpdater() {
             error(app.getString(R.string.ooc_subscription_token_invalid))
         }
 
-        val oocHttpClient = if (certSha256.isNullOrBlank()) httpClient else httpClient.newBuilder()
-            .connectionSpecs(listOf(oocConnSpec))
-            .sslSocketFactory(
-                SSLSocketFactory.getDefault() as SSLSocketFactory, PinnedTrustManager(certSha256)
-            )
-            .build()
-
-        val response = oocHttpClient.newCall(Request.Builder().url(baseLink).header("User-Agent",
-            subscription.customUserAgent.takeIf { it.isNotBlank() } ?: USER_AGENT_ORIGIN).build())
-            .execute()
-            .apply {
-                if (!isSuccessful) error("ERROR: HTTP $code\n\n${body?.string() ?: ""}")
-                if (body == null) error("ERROR: Empty response")
-            }
-
-        Logs.d(response.toString())
+        val response = Libcore.newHttpClient().apply {
+            restrictedTLS()
+            if (certSha256 != null) pinnedSHA256(certSha256)
+        }.newRequest().apply {
+            setURL(baseLink.toString())
+            setUserAgent(subscription.customUserAgent.takeIf { it.isNotBlank() }
+                ?: USER_AGENT_ORIGIN)
+        }.execute()
 
-        val oocResponse = JSONObject(response.body!!.string())
+        val oocResponse = JSONObject(response.contentString)
         subscription.username = oocResponse.getStr("username")
         subscription.bytesUsed = oocResponse.getLong("bytesUsed", -1)
         subscription.bytesRemaining = oocResponse.getLong("bytesRemaining", -1)
@@ -173,7 +155,7 @@ object OpenOnlineConfigUpdater : GroupUpdater() {
             }
         }
 
-        if (subscription.forceResolve) forceResolve(httpClient, profiles, proxyGroup.id)
+        if (subscription.forceResolve) forceResolve(profiles, proxyGroup.id)
 
         val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
         val duplicate = ArrayList<String>()
@@ -294,19 +276,4 @@ object OpenOnlineConfigUpdater : GroupUpdater() {
 
     val supportedProtocols = arrayOf("shadowsocks")
 
-    @SuppressLint("CustomX509TrustManager")
-    class PinnedTrustManager(val certSha256: String) : DefaultTrustManager() {
-
-        override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
-            val serverPK = DigestUtil.sha256Hex(chain[0].publicKey.encoded)
-            if (serverPK != certSha256) throw CertificateException("Excepted certSha256 $certSha256, but was $serverPK")
-        }
-
-        override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
-            val serverPK = DigestUtil.sha256Hex(chain[0].publicKey.encoded)
-            if (serverPK != certSha256) throw CertificateException("Excepted certSha256 $certSha256, but was $serverPK")
-        }
-
-    }
-
 }

+ 13 - 15
app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt

@@ -41,9 +41,7 @@ import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean
 import io.nekohasekai.sagernet.fmt.v2ray.VMessBean
 import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
 import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.OkHttpClient
-import okhttp3.Request
+import libcore.Libcore
 import org.ini4j.Ini
 import org.yaml.snakeyaml.TypeDescription
 import org.yaml.snakeyaml.Yaml
@@ -57,7 +55,6 @@ object RawUpdater : GroupUpdater() {
         proxyGroup: ProxyGroup,
         subscription: SubscriptionBean,
         userInterface: GroupManager.Interface?,
-        httpClient: OkHttpClient,
         byUser: Boolean
     ) {
 
@@ -72,17 +69,18 @@ object RawUpdater : GroupUpdater() {
                 ?: error(app.getString(R.string.no_proxies_found_in_subscription))
         } else {
 
-            val response = httpClient.newCall(Request.Builder()
-                .url(subscription.link.toHttpUrl())
-                .header("User-Agent",
-                    subscription.customUserAgent.takeIf { it.isNotBlank() } ?: USER_AGENT)
-                .build()).execute().apply {
-                if (!isSuccessful) error("ERROR: HTTP $code\n\n${body?.string() ?: ""}")
-                if (body == null) error("ERROR: Empty response")
-            }
+            val response = Libcore.newHttpClient().apply {
+                trySocks5(DataStore.socksPort)
+            }.newRequest().apply {
+                setURL(subscription.link)
+                if (subscription.customUserAgent.isNotBlank()) {
+                    setUserAgent(subscription.customUserAgent)
+                } else {
+                    randomUserAgent()
+                }
+            }.execute()
 
-            Logs.d(response.toString())
-            proxies = parseRaw(response.body!!.string())
+            proxies = parseRaw(response.contentString)
                 ?: error(app.getString(R.string.no_proxies_found))
 
         }
@@ -102,7 +100,7 @@ object RawUpdater : GroupUpdater() {
         }
         proxies = proxiesMap.values.toList()
 
-        if (subscription.forceResolve) forceResolve(httpClient, proxies, proxyGroup.id)
+        if (subscription.forceResolve) forceResolve(proxies, proxyGroup.id)
 
         val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
         val duplicate = ArrayList<String>()

+ 14 - 17
app/src/main/java/io/nekohasekai/sagernet/group/SIP008Updater.kt

@@ -27,11 +27,9 @@ import io.nekohasekai.sagernet.database.*
 import io.nekohasekai.sagernet.fmt.AbstractBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.parseShadowsocks
 import io.nekohasekai.sagernet.ktx.Logs
-import io.nekohasekai.sagernet.ktx.USER_AGENT
 import io.nekohasekai.sagernet.ktx.app
 import io.nekohasekai.sagernet.ktx.applyDefaultValues
-import okhttp3.OkHttpClient
-import okhttp3.Request
+import libcore.Libcore
 
 object SIP008Updater : GroupUpdater() {
 
@@ -39,7 +37,6 @@ object SIP008Updater : GroupUpdater() {
         proxyGroup: ProxyGroup,
         subscription: SubscriptionBean,
         userInterface: GroupManager.Interface?,
-        httpClient: OkHttpClient,
         byUser: Boolean
     ) {
 
@@ -54,19 +51,19 @@ object SIP008Updater : GroupUpdater() {
                 ?: error(app.getString(R.string.no_proxies_found_in_subscription))
         } else {
 
-            val response = httpClient.newCall(Request.Builder()
-                .url(subscription.link)
-                .header("User-Agent",
-                    subscription.customUserAgent.takeIf { it.isNotBlank() } ?: USER_AGENT)
-                .build()).execute().apply {
-                if (!isSuccessful) error("ERROR: HTTP $code\n\n${body?.string() ?: ""}")
-                if (body == null) error("ERROR: Empty response")
-            }
-
-            Logs.d(response.toString())
-
-            sip008Response = JSONObject(response.body!!.string())
+            val response = Libcore.newHttpClient().apply {
+                modernTLS()
+                trySocks5(DataStore.socksPort)
+            }.newRequest().apply {
+                setURL(subscription.link)
+                if (subscription.customUserAgent.isNotBlank()) {
+                    setUserAgent(subscription.customUserAgent)
+                } else {
+                    randomUserAgent()
+                }
+            }.execute()
 
+            sip008Response = JSONObject(response.contentString)
         }
 
         subscription.bytesUsed = sip008Response.getLong("bytesUsed", -1)
@@ -83,7 +80,7 @@ object SIP008Updater : GroupUpdater() {
             profiles.add(bean)
         }
 
-        if (subscription.forceResolve) forceResolve(httpClient, profiles, proxyGroup.id)
+        if (subscription.forceResolve) forceResolve(profiles, proxyGroup.id)
 
         val exists = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
         val duplicate = ArrayList<String>()

+ 0 - 32
app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt

@@ -21,47 +21,15 @@
 
 package io.nekohasekai.sagernet.ktx
 
-import android.os.Build
 import cn.hutool.core.lang.Validator
 import io.nekohasekai.sagernet.BuildConfig
-import io.nekohasekai.sagernet.SagerNet
 import io.nekohasekai.sagernet.bg.VpnService
-import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.AbstractBean
-import io.nekohasekai.sagernet.fmt.LOCALHOST
-import okhttp3.ConnectionSpec
 import okhttp3.HttpUrl
-import okhttp3.OkHttpClient
 import java.net.InetAddress
 import java.net.InetSocketAddress
-import java.net.Proxy
 import java.net.Socket
 
-val okHttpClient = OkHttpClient.Builder()
-    .followRedirects(true)
-    .followSslRedirects(true)
-    .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.RESTRICTED_TLS))
-    .build()
-
-private lateinit var proxyClient: OkHttpClient
-fun createProxyClient(): OkHttpClient {
-    if (!SagerNet.started) return okHttpClient
-
-    if (!::proxyClient.isInitialized) {
-        proxyClient = okHttpClient.newBuilder().proxy(requireProxy()).build()
-    }
-    return proxyClient
-}
-
-
-fun requireProxy(): Proxy {
-    return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
-        Proxy(Proxy.Type.SOCKS, InetSocketAddress(LOCALHOST, DataStore.socksPort))
-    } else {
-        Proxy(Proxy.Type.HTTP, InetSocketAddress(LOCALHOST, DataStore.httpPort))
-    }
-}
-
 fun linkBuilder() = HttpUrl.Builder().scheme("https")
 
 fun HttpUrl.Builder.toLink(scheme: String, appendDefaultPort: Boolean = true): String {

+ 2 - 2
app/src/main/java/io/nekohasekai/sagernet/ktx/Signatures.kt

@@ -30,7 +30,7 @@ import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
 import android.content.pm.Signature
 import android.os.Build
 import android.os.Process
-import cn.hutool.crypto.digest.DigestUtil
+import libcore.Libcore
 
 val devKeys = arrayOf(
     "32250A4B5F3A6733DF57A3B9EC16C38D2C7FC5F2F693A9636F8F7B3BE3549641"
@@ -48,7 +48,7 @@ fun Context.getSignature(): Signature {
 }
 
 fun Context.getSha256Signature(): String {
-    return DigestUtil.sha256Hex(getSignature().toByteArray()).uppercase()
+    return Libcore.sha256Hex(getSignature().toByteArray())
 }
 
 fun Context.isVerified(): Boolean {

+ 2 - 3
app/src/main/java/io/nekohasekai/sagernet/ktx/UUIDs.kt

@@ -21,8 +21,7 @@ package io.nekohasekai.sagernet.ktx
 
 import cn.hutool.core.lang.UUID
 import cn.hutool.core.util.ArrayUtil
-import cn.hutool.crypto.digest.DigestUtil
-import java.io.ByteArrayInputStream
+import libcore.Libcore
 import java.io.ByteArrayOutputStream
 import kotlin.experimental.and
 import kotlin.experimental.or
@@ -31,7 +30,7 @@ fun uuid5(text: String): String {
     val data = ByteArrayOutputStream()
     data.write(ByteArray(16))
     data.write(text.toByteArray())
-    val hash = DigestUtil.sha1(ByteArrayInputStream(data.toByteArray()))
+    val hash = Libcore.sha1(data.toByteArray())
     val result = ArrayUtil.sub(hash, 0, 16)
     result[6] = result[6] and 0x0F.toByte()
     result[6] = result[6] or 0x50.toByte()

+ 40 - 39
app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt

@@ -275,8 +275,6 @@ class AssetsActivity : ThemedActivity() {
     }
 
     suspend fun updateAsset(file: File, versionFile: File, localVersion: String) {
-        val okHttpClient = createProxyClient()
-
         val repo: String
         var fileName = file.name
         if (DataStore.rulesProvider == 0) {
@@ -291,54 +289,57 @@ class AssetsActivity : ThemedActivity() {
             repo = "Loyalsoldier/v2ray-rules-dat"
         }
 
-        var response = okHttpClient.newCall(
-            Request.Builder().url("https://api.github.com/repos/$repo/releases/latest").build()
-        ).execute()
-
-        if (!response.isSuccessful) {
-            error("Error when fetching latest release of $repo : HTTP ${response.code}\n\n${response.body?.string()}")
+        val client = Libcore.newHttpClient().apply {
+            modernTLS()
+            keepAlive()
+            trySocks5(DataStore.socksPort)
         }
 
-        val release = JSONObject(response.body!!.string())
-        val tagName = release.getStr("tag_name")
+        try {
+            var response = client.newRequest().apply {
+                setURL("https://api.github.com/repos/$repo/releases/latest")
+            }.execute()
 
-        if (tagName == localVersion) {
-            onMainDispatcher {
-                snackbar(R.string.route_asset_no_update).show()
+            val release = JSONObject(response.contentString)
+            val tagName = release.getStr("tag_name")
+
+            if (tagName == localVersion) {
+                onMainDispatcher {
+                    snackbar(R.string.route_asset_no_update).show()
+                }
+                return
             }
-            return
-        }
 
-        val releaseAssets = release.getJSONArray("assets").filterIsInstance<JSONObject>()
-        val assetToDownload = releaseAssets.find { it.getStr("name") == fileName }
-            ?: error("File $fileName not found in release ${release["url"]}")
-        val browserDownloadUrl = assetToDownload.getStr("browser_download_url")
+            val releaseAssets = release.getJSONArray("assets").filterIsInstance<JSONObject>()
+            val assetToDownload = releaseAssets.find { it.getStr("name") == fileName }
+                ?: error("File $fileName not found in release ${release["url"]}")
+            val browserDownloadUrl = assetToDownload.getStr("browser_download_url")
 
-        response = okHttpClient.newCall(
-            Request.Builder().url(browserDownloadUrl).build()
-        ).execute()
+            response = client.newRequest().apply {
+                setURL(browserDownloadUrl)
+            }.execute()
 
-        if (!response.isSuccessful) {
-            error("Error when downloading $browserDownloadUrl : HTTP ${response.code}")
-        }
+            val cacheFile = File(file.parentFile, file.name + ".tmp")
+            cacheFile.parentFile?.mkdirs()
 
-        val cacheFile = File(file.parentFile, file.name + ".tmp")
-        response.body!!.use { body ->
-            body.byteStream().use(cacheFile.outputStream())
-        }
-        if (fileName.endsWith(".xz")) {
-            Libcore.unxz(cacheFile.absolutePath, file.absolutePath)
-            cacheFile.delete()
-        } else {
-            cacheFile.renameTo(file)
-        }
+            response.writeTo(cacheFile.canonicalPath)
+
+            if (fileName.endsWith(".xz")) {
+                Libcore.unxz(cacheFile.absolutePath, file.absolutePath)
+                cacheFile.delete()
+            } else {
+                cacheFile.renameTo(file)
+            }
 
-        versionFile.writeText(tagName)
+            versionFile.writeText(tagName)
 
-        adapter.reloadAssets()
+            adapter.reloadAssets()
 
-        onMainDispatcher {
-            snackbar(R.string.route_asset_updated).show()
+            onMainDispatcher {
+                snackbar(R.string.route_asset_updated).show()
+            }
+        } finally {
+            client.close()
         }
     }
 

+ 2 - 4
app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt

@@ -27,10 +27,7 @@ import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.database.ProfileManager
 import io.nekohasekai.sagernet.databinding.LayoutNetworkBinding
 import io.nekohasekai.sagernet.databinding.LayoutProgressBinding
-import io.nekohasekai.sagernet.ktx.app
-import io.nekohasekai.sagernet.ktx.onMainDispatcher
-import io.nekohasekai.sagernet.ktx.readableMessage
-import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
+import io.nekohasekai.sagernet.ktx.*
 import io.nekohasekai.sagernet.utils.Cloudflare
 import io.noties.markwon.Markwon
 import kotlinx.coroutines.Job
@@ -91,6 +88,7 @@ class NetworkFragment : NamedFragment(R.layout.layout_network) {
                     ProfileManager.createProfile(groupId, bean)
                 }
             } catch (e: Exception) {
+                Logs.w(e)
                 onMainDispatcher {
                     if (isActive) {
                         dialog.dismiss()

+ 15 - 24
app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt

@@ -88,31 +88,22 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
         val requireHttp = findPreference<SwitchPreference>(Key.REQUIRE_HTTP)!!
         val appendHttpProxy = findPreference<SwitchPreference>(Key.APPEND_HTTP_PROXY)!!
         val portHttp = findPreference<EditTextPreference>(Key.HTTP_PORT)!!
-        when {
-            Build.VERSION.SDK_INT < Build.VERSION_CODES.N -> {
-                requireHttp.remove()
-                appendHttpProxy.remove()
-                portHttp.setIcon(R.drawable.ic_baseline_http_24)
-                portHttp.onPreferenceChangeListener = reloadListener
-            }
-            Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> {
-                portHttp.isEnabled = requireHttp.isChecked
-                appendHttpProxy.remove()
-                requireHttp.setOnPreferenceChangeListener { _, newValue ->
-                    portHttp.isEnabled = newValue as Boolean
-                    needReload()
-                    true
-                }
+
+        portHttp.isEnabled = requireHttp.isChecked
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            appendHttpProxy.remove()
+            requireHttp.setOnPreferenceChangeListener { _, newValue ->
+                portHttp.isEnabled = newValue as Boolean
+                needReload()
+                true
             }
-            else -> {
-                portHttp.isEnabled = requireHttp.isChecked
-                appendHttpProxy.isEnabled = requireHttp.isChecked
-                requireHttp.setOnPreferenceChangeListener { _, newValue ->
-                    portHttp.isEnabled = newValue as Boolean
-                    appendHttpProxy.isEnabled = newValue as Boolean
-                    needReload()
-                    true
-                }
+        } else {
+            appendHttpProxy.isEnabled = requireHttp.isChecked
+            requireHttp.setOnPreferenceChangeListener { _, newValue ->
+                portHttp.isEnabled = newValue as Boolean
+                appendHttpProxy.isEnabled = newValue as Boolean
+                needReload()
+                true
             }
         }
 

+ 34 - 29
app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt

@@ -19,16 +19,16 @@
 package io.nekohasekai.sagernet.utils
 
 import com.wireguard.crypto.KeyPair
+import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.gson.gson
 import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
-import io.nekohasekai.sagernet.ktx.createProxyClient
+import io.nekohasekai.sagernet.ktx.Logs
 import io.nekohasekai.sagernet.utils.cf.DeviceResponse
 import io.nekohasekai.sagernet.utils.cf.RegisterRequest
 import io.nekohasekai.sagernet.utils.cf.UpdateDeviceRequest
-import okhttp3.MediaType.Companion.toMediaType
+import libcore.Libcore
+import okhttp3.OkHttp
 import okhttp3.Request
-import okhttp3.RequestBody.Companion.toRequestBody
-import okhttp3.internal.closeQuietly
 
 // kang from wgcf
 object Cloudflare {
@@ -41,31 +41,36 @@ object Cloudflare {
 
     fun makeWireGuardConfiguration(): WireGuardBean {
         val keyPair = KeyPair()
-        val okhttpClient = createProxyClient()
-        var body = RegisterRequest.newRequest(keyPair.publicKey)
-        var response = okhttpClient.newCall(
-            Request.Builder()
-                .url("$API_URL/$API_VERSION/reg")
-                .header("Accept", "application/json")
-                .header(CLIENT_VERSION_KEY, CLIENT_VERSION)
-                .post(body.toRequestBody("application/json".toMediaType()))
-                .build()
-        ).execute()
-        if (!response.isSuccessful) error(response)
-        val device = gson.fromJson(response.body!!.string(), DeviceResponse::class.java)
-        val accessToken = device.token
-        body = UpdateDeviceRequest.newRequest()
-        response = okhttpClient.newCall(
-            Request.Builder()
-                .url(API_URL + "/" + API_VERSION + "/reg/" + device.id + "/account/reg/" + device.id)
-                .header("Authorization", "Bearer $accessToken")
-                .header("Accept", "application/json")
-                .header(CLIENT_VERSION_KEY, CLIENT_VERSION)
-                .patch(body.toRequestBody("application/json".toMediaType()))
-                .build()
-        ).execute()
+        val client = Libcore.newHttpClient().apply {
+            pinnedTLS12()
+            trySocks5(DataStore.socksPort)
+        }
+
         try {
-            if (!response.isSuccessful) error(response)
+            val response = client.newRequest().apply {
+                setMethod("POST")
+                setURL("$API_URL/$API_VERSION/reg")
+                setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION)
+                setHeader("Accept", "application/json")
+                setHeader("Content-Type", "application/json")
+                setContentString(RegisterRequest.newRequest(keyPair.publicKey))
+                setUserAgent("okhttp/3.12.1")
+            }.execute()
+
+            val device = gson.fromJson(response.contentString, DeviceResponse::class.java)
+            val accessToken = device.token
+
+            client.newRequest().apply {
+                setMethod("PATCH")
+                setURL(API_URL + "/" + API_VERSION + "/reg/" + device.id + "/account/reg/" + device.id)
+                setHeader("Accept", "application/json")
+                setHeader("Content-Type", "application/json")
+                setHeader("Authorization", "Bearer $accessToken")
+                setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION)
+                setContentString(UpdateDeviceRequest.newRequest())
+                setUserAgent("okhttp/3.12.1")
+            }.execute()
+
             val peer = device.config.peers[0]
             val localAddresses = device.config.interfaceX.addresses
             return WireGuardBean().apply {
@@ -77,7 +82,7 @@ object Cloudflare {
                 localAddress = localAddresses.v4 + "\n" + localAddresses.v6
             }
         } finally {
-            response.body?.closeQuietly()
+            client.close()
         }
     }
 

+ 0 - 162
app/src/main/java/io/nekohasekai/sagernet/utils/HttpsTest.kt

@@ -1,162 +0,0 @@
-/******************************************************************************
- *                                                                            *
- * Copyright (C) 2021 by nekohasekai <[email protected]>             *
- * Copyright (C) 2021 by Max Lv <[email protected]>                          *
- * Copyright (C) 2021 by Mygod Studio <[email protected]>  *
- *                                                                            *
- * This program is free software: you can redistribute it and/or modify       *
- * it under the terms of the GNU General Public License as published by       *
- * the Free Software Foundation, either version 3 of the License, or          *
- *  (at your option) any later version.                                       *
- *                                                                            *
- * This program is distributed in the hope that it will be useful,            *
- * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
- * GNU General Public License for more details.                               *
- *                                                                            *
- * You should have received a copy of the GNU General Public License          *
- * along with this program. If not, see <http://www.gnu.org/licenses/>.       *
- *                                                                            *
- ******************************************************************************/
-
-package io.nekohasekai.sagernet.utils
-
-import android.os.SystemClock
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import io.nekohasekai.sagernet.R
-import io.nekohasekai.sagernet.database.DataStore
-import io.nekohasekai.sagernet.ktx.*
-import kotlinx.coroutines.delay
-import okhttp3.Call
-import okhttp3.OkHttpClient
-import okhttp3.Request
-import okhttp3.internal.closeQuietly
-import java.io.IOException
-
-/**
- * Based on: https://android.googlesource.com/platform/frameworks/base/+/b19a838/services/core/java/com/android/server/connectivity/NetworkMonitor.java#1071
- */
-class HttpsTest : ViewModel() {
-    sealed class Status {
-        protected abstract val status: CharSequence
-        open fun retrieve(setStatus: (CharSequence) -> Unit, errorCallback: (String) -> Unit) =
-            setStatus(status)
-
-        object Idle : Status() {
-            override val status get() = app.getText(R.string.vpn_connected)
-        }
-
-        object Testing : Status() {
-            override val status get() = app.getText(R.string.connection_test_testing)
-        }
-
-        class Success(private val elapsed: Long) : Status() {
-            override val status
-                get() = app.getString(
-                    if (DataStore.connectionTestURL.startsWith("https://")) {
-                        R.string.connection_test_available
-                    } else {
-                        R.string.connection_test_available_http
-                    }, elapsed
-                )
-        }
-
-        sealed class Error : Status() {
-            override val status get() = app.getText(R.string.connection_test_fail)
-            protected abstract val error: String
-            private var shown = false
-            override fun retrieve(
-                setStatus: (CharSequence) -> Unit,
-                errorCallback: (String) -> Unit,
-            ) {
-                super.retrieve(setStatus, errorCallback)
-                if (shown) return
-                shown = true
-                errorCallback(error)
-            }
-
-            class UnexpectedResponseCode(private val code: Int) : Error() {
-                override val error
-                    get() = app.getString(
-                        R.string.connection_test_error_status_code, code
-                    )
-            }
-
-            class IOFailure(private val e: IOException) : Error() {
-                override val error get() = app.getString(R.string.connection_test_error, e.message)
-            }
-        }
-
-    }
-
-    private var running: Call? = null
-    val status = MutableLiveData<Status>(Status.Idle)
-    val okHttpClient by lazy { OkHttpClient.Builder().proxy(requireProxy()).build() }
-
-    fun testConnection() {
-        cancelTest()
-        status.value = Status.Testing
-
-        runOnDefaultDispatcher {
-            val start = SystemClock.elapsedRealtime()
-            running = okHttpClient.newCall(
-                Request.Builder()
-                    .url(DataStore.connectionTestURL)
-                    .addHeader("Connection", "close")
-                    .addHeader("User-Agent", USER_AGENT)
-                    .build()
-            ).apply {
-                val response = try {
-                    execute()
-                } catch (e: IOException) {
-                    if (e.readableMessage.contains("failed to connect to /127.0.0.1") && e.readableMessage.contains(
-                            "ECONNREFUSED"
-                        )
-                    ) {
-                        delay(1000L)
-                        onMainDispatcher {
-                            testConnection()
-                        }
-                        return@runOnDefaultDispatcher
-                    }
-                    if (!isCanceled()) {
-                        onMainDispatcher {
-                            status.value = Status.Error.IOFailure(e)
-                            running = null
-                        }
-                    }
-                    return@runOnDefaultDispatcher
-                }
-
-                if (isCanceled()) {
-                    return@runOnDefaultDispatcher
-                }
-
-                val code = response.code
-                val elapsed = SystemClock.elapsedRealtime() - start
-                response.closeQuietly()
-                runOnMainDispatcher {
-                    status.value = if (code == 204 || code == 200) {
-                        Status.Success(elapsed)
-                    } else {
-                        Status.Error.UnexpectedResponseCode(code)
-                    }
-                    running = null
-                }
-            }
-        }
-
-    }
-
-    private fun cancelTest() {
-        running?.cancel()
-        running = null
-    }
-
-    fun invalidate() {
-        cancelTest()
-        status.value = Status.Idle
-    }
-
-}

+ 1 - 1
library/core

@@ -1 +1 @@
-Subproject commit d6f5fd8f05e72c3cdf2fad82aacfeacec68ab625
+Subproject commit 5adedfdfdf463097fd718bc319fd70badbd7bb78