Browse Source

No more okhttp

世界 3 years ago
parent
commit
d1f1d687c8
23 changed files with 296 additions and 375 deletions
  1. 0 1
      app/build.gradle.kts
  2. 87 177
      app/src/main/java/io/nekohasekai/sagernet/fmt/brook/BrookFmt.kt
  3. 18 20
      app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpFmt.kt
  4. 9 7
      app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt
  5. 13 9
      app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt
  6. 7 9
      app/src/main/java/io/nekohasekai/sagernet/fmt/pingtunnel/PingTunnelFmt.kt
  7. 8 8
      app/src/main/java/io/nekohasekai/sagernet/fmt/relaybaton/RelayBatonFmt.kt
  8. 19 18
      app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt
  9. 20 10
      app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocksr/ShadowsocksRFmt.kt
  10. 15 22
      app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt
  11. 12 8
      app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanFmt.kt
  12. 14 8
      app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt
  13. 15 24
      app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt
  14. 7 14
      app/src/main/java/io/nekohasekai/sagernet/group/OpenOnlineConfigUpdater.kt
  15. 10 12
      app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt
  16. 6 3
      app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt
  17. 0 1
      app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
  18. 12 5
      app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
  19. 0 3
      app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt
  20. 9 5
      app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt
  21. 10 4
      app/src/main/java/io/nekohasekai/sagernet/widget/LinkPreference.kt
  22. 5 6
      app/src/main/java/io/nekohasekai/sagernet/widget/OOCv1TokenPreference.kt
  23. 0 1
      buildSrc/src/main/kotlin/Helpers.kt

+ 0 - 1
app/build.gradle.kts

@@ -59,7 +59,6 @@ dependencies {
     implementation("com.google.code.gson:gson:2.8.9")
     implementation("com.google.zxing:core:3.4.1")
 
-    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")

+ 87 - 177
app/src/main/java/io/nekohasekai/sagernet/fmt/brook/BrookFmt.kt

@@ -21,214 +21,124 @@ package io.nekohasekai.sagernet.fmt.brook
 
 import io.nekohasekai.sagernet.fmt.AbstractBean
 import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
-import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-
-val kinds = arrayOf("server", "wsserver", "wssserver", "socks5")
+import io.nekohasekai.sagernet.ktx.pathSafe
+import io.nekohasekai.sagernet.ktx.queryParameter
+import io.nekohasekai.sagernet.ktx.wrapUri
+import libcore.Libcore
 
 fun parseBrook(text: String): AbstractBean {
-    if (!(text.contains("([?@])".toRegex()))) {
-
-        // https://txthinking.github.io/brook/#/brook-link
-        // old brook scheme
-        var server = text.substringAfter("brook://").unUrlSafe()
-        if (server.startsWith("socks5://")) {
-            server = server.substringAfter("://")
-            val bean = SOCKSBean()
-            bean.serverAddress = server.substringBefore(":")
-            bean.serverPort = server.substringAfter(":").substringBefore(" ").toInt()
-            server = server.substringAfter(":").substringAfter(" ")
-            if (server.contains(" ")) {
-                bean.username = server.substringBefore(" ")
-                bean.password = server.substringAfter(" ")
-            }
-            return bean.applyDefaultValues()
-        }
+    // https://github.com/txthinking/brook/issues/811
 
-        val bean = BrookBean()
+    val link = Libcore.parseURL(text)
 
-        when {
-            server.startsWith("ws://") -> {
-                bean.protocol = "ws"
-                server = server.substringAfter("://")
-            }
-            server.startsWith("wss://") -> {
-                bean.protocol = "wss"
-                server = server.substringAfter("://")
-            }
-            else -> {
-                bean.protocol = ""
-            }
-        }
+    val bean = if (link.host == "socks5") SOCKSBean() else BrookBean()
+    bean.name = link.queryParameter("remarks")
 
-        if (server.contains(" ")) {
-            bean.password = server.substringAfter(" ")
-            server = server.substringBefore(" ")
-        }
+    when (link.host) {
+        "server" -> {
+            bean as BrookBean
+            bean.protocol = ""
 
-        val url =
-            "https://$server".toHttpUrlOrNull() ?: error("Invalid brook link: $text ($server)")
+            val server = link.queryParameter("server")
+                ?: error("Invalid brook server url (Missing server parameter): $text")
 
-        bean.serverAddress = url.host
-        bean.serverPort = url.port
-        //  bean.name = url.fragment
-        if (server.contains("/")) {
-            bean.wsPath = url.encodedPath.unUrlSafe()
+            bean.serverAddress = server.substringBefore(":")
+            bean.serverPort = server.substringAfter(":").toInt()
+            bean.password = link.queryParameter("password")
+                ?: error("Invalid brook server url (Missing password parameter): $text")
         }
-
-        return bean.applyDefaultValues()
-    } else if (text.matches("^brook://(${kinds.joinToString("|")})\\?.+".toRegex())) {
-
-        // https://github.com/txthinking/brook/issues/811
-
-        val link = ("https://" + text.substringAfter("://")).toHttpUrlOrNull()
-            ?: error("Invalid brook url: $text")
-
-        val bean = if (link.host == "socks5") SOCKSBean() else BrookBean()
-        bean.name = link.queryParameter("remarks")
-
-        when (link.host) {
-            "server" -> {
-                bean as BrookBean
-                bean.protocol = ""
-
-                val server = link.queryParameter("server")
-                    ?: error("Invalid brook server url (Missing server parameter): $text")
-
-                bean.serverAddress = server.substringBefore(":")
-                bean.serverPort = server.substringAfter(":").toInt()
-                bean.password = link.queryParameter("password")
-                    ?: error("Invalid brook server url (Missing password parameter): $text")
+        "wsserver" -> {
+            bean as BrookBean
+            bean.protocol = "ws"
+
+
+            var wsserver = (link.queryParameter("wsserver")
+                ?: error("Invalid brook wsserver url (Missing wsserver parameter): $text")).substringAfter(
+                "://"
+            )
+            if (wsserver.contains("/")) {
+                bean.wsPath = "/" + wsserver.substringAfter("/")
+                wsserver = wsserver.substringBefore("/")
             }
-            "wsserver" -> {
-                bean as BrookBean
-                bean.protocol = "ws"
-
-
-                var wsserver = (link.queryParameter("wsserver")
-                    ?: error("Invalid brook wsserver url (Missing wsserver parameter): $text"))
-                    .substringAfter("://")
-                if (wsserver.contains("/")) {
-                    bean.wsPath = "/" + wsserver.substringAfter("/")
-                    wsserver = wsserver.substringBefore("/")
-                }
-                bean.serverAddress = wsserver.substringBefore(":")
-                bean.serverPort = wsserver.substringAfter(":").toInt()
-                bean.password = link.queryParameter("password")
-                    ?: error("Invalid brook wsserver url (Missing password parameter): $text")
-
-            }
-            "wssserver" -> {
-                bean as BrookBean
-                bean.protocol = "wss"
-
-
-                var wsserver = (link.queryParameter("wssserver")
-                    ?: error("Invalid brook wssserver url (Missing wssserver parameter): $text"))
-                    .substringAfter("://")
-                if (wsserver.contains("/")) {
-                    bean.wsPath = "/" + wsserver.substringAfter("/")
-                    wsserver = wsserver.substringBefore("/")
-                }
-                bean.serverAddress = wsserver.substringBefore(":")
-                bean.serverPort = wsserver.substringAfter(":").toInt()
-                bean.password = link.queryParameter("password")
-                    ?: error("Invalid brook wssserver url (Missing password parameter): $text")
+            bean.serverAddress = wsserver.substringBefore(":")
+            bean.serverPort = wsserver.substringAfter(":").toInt()
+            bean.password = link.queryParameter("password")
+                ?: error("Invalid brook wsserver url (Missing password parameter): $text")
 
+        }
+        "wssserver" -> {
+            bean as BrookBean
+            bean.protocol = "wss"
+
+
+            var wsserver = (link.queryParameter("wssserver")
+                ?: error("Invalid brook wssserver url (Missing wssserver parameter): $text")).substringAfter(
+                "://"
+            )
+            if (wsserver.contains("/")) {
+                bean.wsPath = "/" + wsserver.substringAfter("/")
+                wsserver = wsserver.substringBefore("/")
             }
-            "socks5" -> {
-                bean as SOCKSBean
+            bean.serverAddress = wsserver.substringBefore(":")
+            bean.serverPort = wsserver.substringAfter(":").toInt()
+            bean.password = link.queryParameter("password")
+                ?: error("Invalid brook wssserver url (Missing password parameter): $text")
 
-                val socks5 = (link.queryParameter("socks5")
-                    ?: error("Invalid brook socks5 url (Missing socks5 parameter): $text"))
-                    .substringAfter("://")
-
-                bean.serverAddress = socks5.substringBefore(":")
-                bean.serverPort = socks5.substringAfter(":").toInt()
-
-                link.queryParameter("username")?.also { username ->
-                    bean.username = username
-
-                    link.queryParameter("password")?.also { password ->
-                        bean.password = password
-                    }
-                }
-            }
         }
+        "socks5" -> {
+            bean as SOCKSBean
 
-        return bean
-
-    } else {
+            val socks5 = (link.queryParameter("socks5")
+                ?: error("Invalid brook socks5 url (Missing socks5 parameter): $text")).substringAfter(
+                "://"
+            )
 
-        /**
-         * brook://urlEncode(password)@host:port#urlEncode(remarks)
-         * brook+ws(s)://urlEncode(password)@host:port?path=...#urlEncode(remarks)
-         */
-        val proto = if (!text.startsWith("brook+")) "" else {
-            text.substringAfter("+").substringBefore("://")
-        }
+            bean.serverAddress = socks5.substringBefore(":")
+            bean.serverPort = socks5.substringAfter(":").toInt()
 
-        if (proto !in arrayOf("", "ws", "wss")) error("Invalid brook protocol $proto")
+            link.queryParameter("username")?.also { username ->
+                bean.username = username
 
-        val link = ("https://" + text.substringAfter("://")).toHttpUrlOrNull()
-            ?: error("Invalid brook url: $text")
-
-        return BrookBean().apply {
-            protocol = proto
-            serverAddress = link.host
-            serverPort = link.port
-            password = link.username
-            link.queryParameter("path")?.also {
-                wsPath = it
+                link.queryParameter("password")?.also { password ->
+                    bean.password = password
+                }
             }
-            name = link.fragment
         }
     }
+
+    return bean
 }
 
 fun BrookBean.toUri(): String {
-    /*var server = when (protocol) {
-        "ws" -> "ws://$serverAddress:$serverPort"
-        "wss" -> "wss://$serverAddress:$serverPort"
-        else -> "$serverAddress:$serverPort"
-    }
+    val builder = Libcore.newURL("brook")
+    var serverString = "$serverAddress:$serverPort"
     if (protocol.startsWith("ws")) {
-        if (wsPath.isNotBlank()) {
-            if (!wsPath.startsWith("/")) {
-                server += "/"
-            }
-            server += wsPath.pathSafe()
+        if (wsPath.isNotBlank() && wsPath != "/") {
+            if (!wsPath.startsWith("/")) wsPath = "/$wsPath"
+            serverString += wsPath
         }
     }
-    //if (name.isNotBlank()) {
-    //    server += "#" + name.urlSafe()
-    //}
-    if (password.isNotBlank()) {
-        server = "$server $password"
+    when (protocol) {
+        "ws" -> {
+            builder.host = "wsserver"
+            builder.addQueryParameter("wsserver", serverString)
+        }
+        "wss" -> {
+            builder.host = "wssserver"
+            builder.addQueryParameter("wssserver", serverString)
+        }
+        else -> {
+            builder.host = "server"
+            builder.addQueryParameter("server", serverString)
+        }
     }
-    return "brook://" + server.urlSafe()*/
-
-    val builder = linkBuilder()
-        .host(serverAddress)
-        .port(serverPort)
-
     if (password.isNotBlank()) {
-        builder.encodedUsername(password.urlSafe())
+        builder.addQueryParameter("password", password)
     }
-
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.addQueryParameter("remarks", name)
     }
-
-    if (wsPath.isNotBlank()) {
-        builder.addQueryParameter("path", wsPath)
-    }
-
-    return when (protocol) {
-        "ws", "wss" -> builder.toLink("brook+$protocol", false)
-        else -> builder.toLink("brook")
-    }
-
+    return builder.string
 }
 
 fun BrookBean.internalUri(): String {

+ 18 - 20
app/src/main/java/io/nekohasekai/sagernet/fmt/http/HttpFmt.kt

@@ -19,44 +19,42 @@
 
 package io.nekohasekai.sagernet.fmt.http
 
+import io.nekohasekai.sagernet.ktx.queryParameter
 import io.nekohasekai.sagernet.ktx.urlSafe
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import libcore.Libcore
 
 fun parseHttp(link: String): HttpBean {
-    val httpUrl = link.toHttpUrlOrNull() ?: error("Invalid http(s) link: $link")
-
-    if (httpUrl.encodedPath != "/") error("Not http proxy")
+    val url = Libcore.parseURL(link)
+    if (url.rawPath != "/") error("Not http proxy")
 
     return HttpBean().apply {
-        serverAddress = httpUrl.host
-        serverPort = httpUrl.port
-        username = httpUrl.username
-        password = httpUrl.password
-        sni = httpUrl.queryParameter("sni")
-        name = httpUrl.fragment
-        tls = httpUrl.scheme == "https"
+        serverAddress = url.host
+        serverPort = url.port
+        username = url.username
+        password = url.password
+        sni = url.queryParameter("sni")
+        name = url.fragment
+        tls = url.scheme == "https"
     }
 }
 
 fun HttpBean.toUri(): String {
-    val builder = HttpUrl.Builder()
-        .scheme(if (tls) "https" else "http")
-        .host(serverAddress)
-        .port(serverPort)
+    val builder = Libcore.newURL(if (tls) "https" else "http")
+    builder.host = serverAddress
+    builder.port = serverPort
 
     if (username.isNotBlank()) {
-        builder.username(username)
+        builder.username = username
     }
     if (password.isNotBlank()) {
-        builder.password(password)
+        builder.password = password
     }
     if (sni.isNotBlank()) {
         builder.addQueryParameter("sni", sni)
     }
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
 
-    return builder.toString()
+    return builder.string
 }

+ 9 - 7
app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt

@@ -24,16 +24,15 @@ import cn.hutool.json.JSONObject
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.LOCALHOST
 import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import libcore.Libcore
 import java.io.File
 
 
 // hysteria://host:port?auth=123456&peer=sni.domain&insecure=1|0&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
 
 fun parseHysteria(url: String): HysteriaBean {
-    val link = url.replace("hysteria://", "https://").toHttpUrlOrNull() ?: error(
-        "invalid hysteria link $url"
-    )
+    val link = Libcore.parseURL(url)
+
     return HysteriaBean().apply {
         serverAddress = link.host
         serverPort = link.port
@@ -70,7 +69,10 @@ fun parseHysteria(url: String): HysteriaBean {
 }
 
 fun HysteriaBean.toUri(): String {
-    val builder = linkBuilder().host(serverAddress).port(serverPort)
+    val builder = Libcore.newURL("hysteria")
+    builder.host = serverAddress
+    builder.port = serverPort
+
     if (sni.isNotBlank()) {
         builder.addQueryParameter("peer", sni)
     }
@@ -102,9 +104,9 @@ fun HysteriaBean.toUri(): String {
         builder.addQueryParameter("protocol", "faketcp")
     }
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
-    return builder.toLink("hysteria")
+    return builder.string
 }
 
 fun JSONObject.parseHysteria(): HysteriaBean {

+ 13 - 9
app/src/main/java/io/nekohasekai/sagernet/fmt/naive/NaiveFmt.kt

@@ -22,13 +22,15 @@ package io.nekohasekai.sagernet.fmt.naive
 import cn.hutool.json.JSONObject
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.LOCALHOST
-import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import io.nekohasekai.sagernet.ktx.isIpAddress
+import io.nekohasekai.sagernet.ktx.queryParameter
+import io.nekohasekai.sagernet.ktx.unUrlSafe
+import io.nekohasekai.sagernet.ktx.urlSafe
+import libcore.Libcore
 
 fun parseNaive(link: String): NaiveBean {
     val proto = link.substringAfter("+").substringBefore(":")
-    val url = ("https://" + link.substringAfter("://")).toHttpUrlOrNull()
-        ?: error("Invalid naive link: $link")
+    val url = Libcore.parseURL(link)
     return NaiveBean().also {
         it.proto = proto
     }.apply {
@@ -44,11 +46,13 @@ fun parseNaive(link: String): NaiveBean {
 }
 
 fun NaiveBean.toUri(proxyOnly: Boolean = false): String {
-    val builder = linkBuilder().host(serverAddress).port(finalPort)
+    val builder = Libcore.newURL(if (proxyOnly) proto else "naive+$proto")
+    builder.host = serverAddress
+    builder.port = serverPort
     if (username.isNotBlank()) {
-        builder.username(username)
+        builder.username = username
         if (password.isNotBlank()) {
-            builder.password(password)
+            builder.password = password
         }
     }
     if (!proxyOnly) {
@@ -56,13 +60,13 @@ fun NaiveBean.toUri(proxyOnly: Boolean = false): String {
             builder.addQueryParameter("extra-headers", extraHeaders)
         }
         if (name.isNotBlank()) {
-            builder.encodedFragment(name.urlSafe())
+            builder.setRawFragment(name.urlSafe())
         }
         if (insecureConcurrency > 0) {
             builder.addQueryParameter("insecure-concurrency", "$insecureConcurrency")
         }
     }
-    return builder.toLink(if (proxyOnly) proto else "naive+$proto", false)
+    return builder.string
 }
 
 fun NaiveBean.buildNaiveConfig(port: Int): String {

+ 7 - 9
app/src/main/java/io/nekohasekai/sagernet/fmt/pingtunnel/PingTunnelFmt.kt

@@ -19,10 +19,8 @@
 
 package io.nekohasekai.sagernet.fmt.pingtunnel
 
-import io.nekohasekai.sagernet.ktx.linkBuilder
-import io.nekohasekai.sagernet.ktx.toLink
 import io.nekohasekai.sagernet.ktx.urlSafe
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import libcore.Libcore
 
 
 /**
@@ -32,8 +30,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
  */
 
 fun parsePingTunnel(server: String): PingTunnelBean {
-    val link = server.replace("ping-tunnel://", "https://").toHttpUrlOrNull()
-        ?: error("invalid PingTunnel link $server")
+    val link = Libcore.parseURL(server)
     return PingTunnelBean().apply {
         serverAddress = link.host
         key = link.username
@@ -45,12 +42,13 @@ fun parsePingTunnel(server: String): PingTunnelBean {
 }
 
 fun PingTunnelBean.toUri(): String {
-    val builder = linkBuilder().host(serverAddress)
+    val builder = Libcore.newURL("ping-tunnel")
+    builder.host = serverAddress
     if (key.isNotBlank() && key != "1") {
-        builder.encodedUsername(key.urlSafe())
+        builder.username = key
     }
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
-    return builder.toLink("ping-tunnel", false)
+    return builder.string
 }

+ 8 - 8
app/src/main/java/io/nekohasekai/sagernet/fmt/relaybaton/RelayBatonFmt.kt

@@ -20,14 +20,11 @@
 package io.nekohasekai.sagernet.fmt.relaybaton
 
 import io.nekohasekai.sagernet.database.DataStore
-import io.nekohasekai.sagernet.ktx.linkBuilder
-import io.nekohasekai.sagernet.ktx.toLink
 import io.nekohasekai.sagernet.ktx.urlSafe
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import libcore.Libcore
 
 fun parseRelayBaton(link: String): RelayBatonBean {
-    val url = (link.replace("relaybaton://", "https://")).toHttpUrlOrNull()
-        ?: error("Invalid relaybaton link: $link")
+    val url = Libcore.parseURL(link)
     return RelayBatonBean().apply {
         serverAddress = url.host
         username = url.username
@@ -38,13 +35,16 @@ fun parseRelayBaton(link: String): RelayBatonBean {
 }
 
 fun RelayBatonBean.toUri(): String {
-    val builder = linkBuilder().host(serverAddress).username(username).password(password)
+    val builder = Libcore.newURL("relaybaton")
+    builder.host = serverAddress
+    builder.username = username
+    builder.password = password
 
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
 
-    return builder.toLink("relaybaton", false)
+    return builder.string
 }
 
 fun RelayBatonBean.buildRelayBatonConfig(port: Int): String {

+ 19 - 18
app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt

@@ -27,8 +27,11 @@ import com.github.shadowsocks.plugin.PluginOptions
 import io.nekohasekai.sagernet.IPv6Mode
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.LOCALHOST
-import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import io.nekohasekai.sagernet.ktx.decodeBase64UrlSafe
+import io.nekohasekai.sagernet.ktx.queryParameter
+import io.nekohasekai.sagernet.ktx.unUrlSafe
+import io.nekohasekai.sagernet.ktx.urlSafe
+import libcore.Libcore
 
 fun PluginConfiguration.fixInvalidParams() {
 
@@ -78,17 +81,13 @@ fun parseShadowsocks(url: String): ShadowsocksBean {
 
     if (url.contains("@")) {
 
-        var link = url.replace("ss://", "https://").toHttpUrlOrNull() ?: error(
-            "invalid ss-android link $url"
-        )
+        var link = Libcore.parseURL(url)
 
         if (link.username.isBlank()) { // fix justmysocks's shit link
-
-            link = (("https://" + url.substringAfter("ss://")
-                .substringBefore("#")
-                .decodeBase64UrlSafe()).toHttpUrlOrNull() ?: error(
-                "invalid jms link $url"
-            )).newBuilder().fragment(url.substringAfter("#")).build()
+            link = Libcore.parseURL(
+                ("ss://" + url.substringAfter("ss://").substringBefore("#").decodeBase64UrlSafe())
+            )
+            link.setRawFragment(url.substringAfter("#"))
         }
 
         // ss-android style
@@ -133,8 +132,9 @@ fun parseShadowsocks(url: String): ShadowsocksBean {
 
         if (v2Url.contains("#")) v2Url = v2Url.substringBefore("#")
 
-        val link = ("https://" + v2Url.substringAfter("ss://")
-            .decodeBase64UrlSafe()).toHttpUrlOrNull() ?: error("invalid v2rayN link $url")
+        val link = Libcore.parseURL(
+            ("ss://" + v2Url.substringAfter("ss://").decodeBase64UrlSafe())
+        )
 
         return ShadowsocksBean().apply {
 
@@ -157,19 +157,20 @@ fun parseShadowsocks(url: String): ShadowsocksBean {
 
 fun ShadowsocksBean.toUri(): String {
 
-    val builder = linkBuilder().username(Base64.encodeUrlSafe("$method:$password"))
-        .host(serverAddress)
-        .port(serverPort)
+    val builder = Libcore.newURL("ss")
+    builder.host = serverAddress
+    builder.port = serverPort
+    builder.username = Base64.encodeUrlSafe("$method:$password")
 
     if (plugin.isNotBlank()) {
         builder.addQueryParameter("plugin", plugin)
     }
 
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
 
-    return builder.toLink("ss").replace("$serverPort/", "$serverPort")
+    return builder.string
 
 }
 

+ 20 - 10
app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocksr/ShadowsocksRFmt.kt

@@ -25,7 +25,8 @@ import io.nekohasekai.sagernet.IPv6Mode
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.ktx.applyDefaultValues
 import io.nekohasekai.sagernet.ktx.decodeBase64UrlSafe
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import io.nekohasekai.sagernet.ktx.queryParameter
+import libcore.Libcore
 import java.util.*
 
 fun parseShadowsocksR(url: String): ShadowsocksRBean {
@@ -41,18 +42,18 @@ fun parseShadowsocksR(url: String): ShadowsocksRBean {
         password = params[5].substringBefore("/").decodeBase64UrlSafe()
     }
 
-    val httpUrl = ("https://localhost" + params[5].substringAfter("/")).toHttpUrl()
+    val httpUrl = Libcore.parseURL("https://localhost" + params[5].substringAfter("/"))
 
-    runCatching {
-        bean.obfsParam = httpUrl.queryParameter("obfsparam")!!.decodeBase64UrlSafe()
+    httpUrl.queryParameter("obfsparam")?.let {
+        bean.obfsParam = it.decodeBase64UrlSafe()
     }
-    runCatching {
-        bean.protocolParam = httpUrl.queryParameter("protoparam")!!.decodeBase64UrlSafe()
+
+    httpUrl.queryParameter("protoparam")?.let {
+        bean.protocolParam = it.decodeBase64UrlSafe()
     }
 
-    val remarks = httpUrl.queryParameter("remarks")
-    if (!remarks.isNullOrBlank()) {
-        bean.name = remarks.decodeBase64UrlSafe()
+    httpUrl.queryParameter("remarks")?.let {
+        bean.name = it.decodeBase64UrlSafe()
     }
 
     return bean
@@ -63,7 +64,16 @@ fun ShadowsocksRBean.toUri(): String {
 
     return "ssr://" + Base64.encodeUrlSafe(
         "%s:%d:%s:%s:%s:%s/?obfsparam=%s&protoparam=%s&remarks=%s".format(
-            Locale.ENGLISH, serverAddress, serverPort, protocol, method, obfs, Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, password)), Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, obfsParam)), Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, protocolParam)), Base64.encodeUrlSafe(
+            Locale.ENGLISH,
+            serverAddress,
+            serverPort,
+            protocol,
+            method,
+            obfs,
+            Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, password)),
+            Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, obfsParam)),
+            Base64.encodeUrlSafe("%s".format(Locale.ENGLISH, protocolParam)),
+            Base64.encodeUrlSafe(
                 "%s".format(
                     Locale.ENGLISH, name ?: ""
                 )

+ 15 - 22
app/src/main/java/io/nekohasekai/sagernet/fmt/socks/SOCKSFmt.kt

@@ -20,34 +20,26 @@
 package io.nekohasekai.sagernet.fmt.socks
 
 import cn.hutool.core.codec.Base64
-import io.nekohasekai.sagernet.ktx.decodeBase64UrlSafe
-import io.nekohasekai.sagernet.ktx.toLink
+import io.nekohasekai.sagernet.ktx.queryParameter
 import io.nekohasekai.sagernet.ktx.unUrlSafe
 import io.nekohasekai.sagernet.ktx.urlSafe
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import libcore.Libcore
 
 fun parseSOCKS(link: String): SOCKSBean {
     if (!link.substringAfter("://").contains(":")) {
         // v2rayN shit format
-        var url = link.substringAfter("://")
-        if (url.contains("#")) {
-            url = url.substringBeforeLast("#")
-        }
-        url = url.decodeBase64UrlSafe()
-        val httpUrl = "http://$url".toHttpUrlOrNull() ?: error("Invalid v2rayN link content: $url")
+        val url = Libcore.parseURL(link)
         return SOCKSBean().apply {
-            serverAddress = httpUrl.host
-            serverPort = httpUrl.port
-            username = httpUrl.username.takeIf { it != "null" } ?: ""
-            password = httpUrl.password.takeIf { it != "null" } ?: ""
+            serverAddress = url.host
+            serverPort = url.port
+            username = url.username.takeIf { it != "null" } ?: ""
+            password = url.password.takeIf { it != "null" } ?: ""
             if (link.contains("#")) {
                 name = link.substringAfter("#").unUrlSafe()
             }
         }
     } else {
-        val url = ("http://" + link.substringAfter("://")).toHttpUrlOrNull()
-            ?: error("Not supported: $link")
+        val url = Libcore.parseURL(link)
 
         return SOCKSBean().apply {
             protocol = when {
@@ -67,18 +59,19 @@ fun parseSOCKS(link: String): SOCKSBean {
 }
 
 fun SOCKSBean.toUri(): String {
-
-    val builder = HttpUrl.Builder().scheme("http").host(serverAddress).port(serverPort)
-    if (!username.isNullOrBlank()) builder.username(username)
-    if (!password.isNullOrBlank()) builder.password(password)
+    val builder = Libcore.newURL("socks${protocolVersion()}")
+    builder.host = serverAddress
+    builder.port = serverPort
+    if (!username.isNullOrBlank()) builder.username = username
+    if (!password.isNullOrBlank()) builder.password = password
     if (tls) {
         builder.addQueryParameter("tls", "true")
         if (sni.isNotBlank()) {
             builder.addQueryParameter("sni", sni)
         }
     }
-    if (!name.isNullOrBlank()) builder.encodedFragment(name.urlSafe())
-    return builder.toLink("socks${protocolVersion()}")
+    if (!name.isNullOrBlank()) builder.setRawFragment(name.urlSafe())
+    return builder.string
 
 }
 

+ 12 - 8
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanFmt.kt

@@ -24,15 +24,17 @@ import cn.hutool.json.JSONObject
 import io.nekohasekai.sagernet.IPv6Mode
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.LOCALHOST
-import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import io.nekohasekai.sagernet.ktx.isExpert
+import io.nekohasekai.sagernet.ktx.isIpAddress
+import io.nekohasekai.sagernet.ktx.queryParameter
+import io.nekohasekai.sagernet.ktx.urlSafe
+import libcore.Libcore
 
 // WTF
 // https://github.com/trojan-gfw/igniter/issues/318
 fun parseTrojan(server: String): TrojanBean {
 
-    val link = server.replace("trojan://", "https://").toHttpUrlOrNull()
-        ?: error("invalid trojan link $server")
+    val link = Libcore.parseURL(server)
 
     return TrojanBean().apply {
         serverAddress = link.host
@@ -57,7 +59,10 @@ fun parseTrojan(server: String): TrojanBean {
 
 fun TrojanBean.toUri(): String {
 
-    val builder = linkBuilder().username(password).host(serverAddress).port(serverPort)
+    val builder = Libcore.newURL("trojan")
+    builder.host = serverAddress
+    builder.port = serverPort
+    builder.username = password
 
     if (sni.isNotBlank()) {
         builder.addQueryParameter("sni", sni)
@@ -76,11 +81,10 @@ fun TrojanBean.toUri(): String {
     }
 
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
 
-
-    return builder.toLink("trojan")
+    return builder.string
 
 }
 

+ 14 - 8
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt

@@ -28,13 +28,15 @@ import io.nekohasekai.sagernet.IPv6Mode
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.LOCALHOST
 import io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams
-import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import io.nekohasekai.sagernet.ktx.applyDefaultValues
+import io.nekohasekai.sagernet.ktx.isIpAddress
+import io.nekohasekai.sagernet.ktx.queryParameter
+import io.nekohasekai.sagernet.ktx.urlSafe
+import libcore.Libcore
 
 fun parseTrojanGo(server: String): TrojanGoBean {
-    val link = server.replace("trojan-go://", "https://").toHttpUrlOrNull() ?: error(
-        "invalid trojan-link link $server"
-    )
+    val link = Libcore.parseURL(server)
+
     return TrojanGoBean().apply {
         serverAddress = link.host
         serverPort = link.port
@@ -71,7 +73,11 @@ fun parseTrojanGo(server: String): TrojanGoBean {
 }
 
 fun TrojanGoBean.toUri(): String {
-    val builder = linkBuilder().username(password).host(serverAddress).port(serverPort)
+    val builder = Libcore.newURL("trojan-go")
+    builder.host = serverAddress
+    builder.port = serverPort
+    builder.username = password
+
     if (sni.isNotBlank()) {
         builder.addQueryParameter("sni", sni)
     }
@@ -97,10 +103,10 @@ fun TrojanGoBean.toUri(): String {
     }
 
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
 
-    return builder.toLink("trojan-go")
+    return builder.string
 }
 
 fun TrojanGoBean.buildTrojanGoConfig(port: Int, mux: Boolean): String {

+ 15 - 24
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayFmt.kt

@@ -23,19 +23,19 @@ import cn.hutool.core.codec.Base64
 import cn.hutool.json.JSONObject
 import com.v2ray.core.common.net.packetaddr.PacketAddrType
 import io.nekohasekai.sagernet.ktx.*
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import libcore.Libcore
 
 fun parseV2Ray(link: String): StandardV2RayBean {
     if (!link.contains("@")) {
         return parseV2RayN(link)
     }
 
-    val bean = if (!link.startsWith("vless://")) {
+    val url = Libcore.parseURL(link)
+    val bean = if (url.scheme == "vmess") {
         VMessBean()
     } else {
         VLESSBean()
     }
-    val url = link.replace("vmess://", "https://").replace("vless://", "https://").toHttpUrl()
 
     bean.serverAddress = url.host
     bean.serverPort = url.port
@@ -110,9 +110,6 @@ fun parseV2Ray(link: String): StandardV2RayBean {
     } else { // https://github.com/XTLS/Xray-core/issues/91
 
         bean.uuid = url.username
-        if (url.pathSegments.size > 1 || url.pathSegments[0].isNotBlank()) {
-            bean.path = url.pathSegments.joinToString("/")
-        }
 
         val protocol = url.queryParameter("type") ?: "tcp"
         bean.type = protocol
@@ -354,14 +351,16 @@ fun VMessBean.toV2rayN(): String {
 
 }
 
-fun StandardV2RayBean.toUri(standard: Boolean = true): String {
+fun StandardV2RayBean.toUri(): String {
 //    if (this is VMessBean && alterId > 0) return toV2rayN()
 
-    val builder = linkBuilder().username(uuid)
-        .host(serverAddress)
-        .port(serverPort)
-        .addQueryParameter("type", type)
-        .addQueryParameter("encryption", encryption)
+    val builder = Libcore.newURL(if (this is VMessBean) "vmess" else "vless")
+    builder.host = serverAddress
+    builder.port = serverPort
+
+
+    builder.addQueryParameter("type", type)
+    builder.addQueryParameter("encryption", encryption)
 
     when (type) {
         "tcp" -> {
@@ -372,11 +371,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
                     builder.addQueryParameter("host", host)
                 }
                 if (path.isNotBlank()) {
-                    if (standard) {
-                        builder.addQueryParameter("path", path)
-                    } else {
-                        builder.encodedPath(path.pathSafe())
-                    }
+                    builder.addQueryParameter("path", path)
                 }
             }
         }
@@ -393,11 +388,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
                 builder.addQueryParameter("host", host)
             }
             if (path.isNotBlank()) {
-                if (standard) {
-                    builder.addQueryParameter("path", path)
-                } else {
-                    builder.encodedPath(path.pathSafe())
-                }
+                builder.addQueryParameter("path", path)
             }
             if (type == "ws") {
                 if (wsMaxEarlyData > 0) {
@@ -454,9 +445,9 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
     }
 
     if (name.isNotBlank()) {
-        builder.encodedFragment(name.urlSafe())
+        builder.setRawFragment(name.urlSafe())
     }
 
-    return builder.toLink(if (this is VMessBean) "vmess" else "vless")
+    return builder.string
 
 }

+ 7 - 14
app/src/main/java/io/nekohasekai/sagernet/group/OpenOnlineConfigUpdater.kt

@@ -29,13 +29,9 @@ import io.nekohasekai.sagernet.database.*
 import io.nekohasekai.sagernet.fmt.AbstractBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.ShadowsocksBean
 import io.nekohasekai.sagernet.fmt.shadowsocks.fixInvalidParams
-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 io.nekohasekai.sagernet.ktx.*
 import libcore.Libcore
-import okhttp3.HttpUrl
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import libcore.URL
 
 object OpenOnlineConfigUpdater : GroupUpdater() {
 
@@ -46,7 +42,7 @@ object OpenOnlineConfigUpdater : GroupUpdater() {
         byUser: Boolean
     ) {
         val apiToken: JSONObject
-        var baseLink: HttpUrl
+        val baseLink: URL
         val certSha256: String?
         try {
             apiToken = JSONObject(subscription.token)
@@ -70,18 +66,15 @@ object OpenOnlineConfigUpdater : GroupUpdater() {
                 !baseUrl.startsWith("https://") -> {
                     error("Protocol scheme must be https")
                 }
-                else -> baseLink = baseUrl.toHttpUrl()
+                else -> baseLink = Libcore.parseURL(baseUrl)
             }
             val secret = apiToken.getStr("secret")
             if (secret.isNullOrBlank()) error("Missing field: secret")
-            baseLink = baseLink.newBuilder()
-                .addPathSegments(secret)
-                .addPathSegments("ooc/v1")
-                .build()
+            baseLink.addPathSegments(secret, "ooc/v1")
 
             val userId = apiToken.getStr("userId")
             if (userId.isNullOrBlank()) error("Missing field: userId")
-            baseLink = baseLink.newBuilder().addPathSegment(userId).build()
+            baseLink.addPathSegments(userId)
             certSha256 = apiToken.getStr("certSha256")
             if (!certSha256.isNullOrBlank()) {
                 when {
@@ -102,7 +95,7 @@ object OpenOnlineConfigUpdater : GroupUpdater() {
             restrictedTLS()
             if (certSha256 != null) pinnedSHA256(certSha256)
         }.newRequest().apply {
-            setURL(baseLink.toString())
+            setURL(baseLink.string)
             setUserAgent(subscription.customUserAgent.takeIf { it.isNotBlank() }
                 ?: USER_AGENT_ORIGIN)
         }.execute()

+ 10 - 12
app/src/main/java/io/nekohasekai/sagernet/ktx/Nets.kt

@@ -25,23 +25,21 @@ import cn.hutool.core.lang.Validator
 import io.nekohasekai.sagernet.BuildConfig
 import io.nekohasekai.sagernet.bg.VpnService
 import io.nekohasekai.sagernet.fmt.AbstractBean
-import okhttp3.HttpUrl
+import libcore.URL
 import java.net.InetAddress
 import java.net.InetSocketAddress
 import java.net.Socket
 
-fun linkBuilder() = HttpUrl.Builder().scheme("https")
-
-fun HttpUrl.Builder.toLink(scheme: String, appendDefaultPort: Boolean = true): String {
-    var url = build()
-    val defaultPort = HttpUrl.defaultPort(url.scheme)
-    var replace = false
-    if (appendDefaultPort && url.port == defaultPort) {
-        url = url.newBuilder().port(14514).build()
-        replace = true
+fun URL.queryParameter(key: String) = queryParameterNotBlank(key).takeIf { it.isNotBlank() }
+var URL.pathSegments: List<String>
+    get() = path.split("/").filter { it.isNotBlank() }
+    set(value) {
+        path = value.joinToString("/")
     }
-    return url.toString().replace("${url.scheme}://", "$scheme://").let {
-        if (replace) it.replace(":14514", ":$defaultPort") else it
+
+fun URL.addPathSegments(vararg segments: String) {
+    pathSegments = pathSegments.toMutableList().apply {
+        addAll(segments)
     }
 }
 

+ 6 - 3
app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt

@@ -61,7 +61,6 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.withContext
-import okhttp3.internal.closeQuietly
 import org.jf.dexlib2.dexbacked.DexBackedDexFile
 import org.jf.dexlib2.iface.DexFile
 import java.io.File
@@ -435,7 +434,9 @@ class AppManagerActivity : ThemedActivity() {
                                         app to app.applicationInfo.loadLabel(packageManager)
                                             .toString()
                                     )
-                                    zipFile.closeQuietly()
+                                    runCatching {
+                                        zipFile.close()
+                                    }
 
                                     if (bypass) {
                                         changed = !proxiedUids[app.applicationInfo.uid]
@@ -449,7 +450,9 @@ class AppManagerActivity : ThemedActivity() {
                             }
                         }
                     }
-                    zipFile.closeQuietly()
+                    runCatching {
+                        zipFile.close()
+                    }
 
                     if (bypass) {
                         proxiedUids.delete(app.applicationInfo.uid)

+ 0 - 1
app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt

@@ -38,7 +38,6 @@ import io.nekohasekai.sagernet.databinding.LayoutAssetsBinding
 import io.nekohasekai.sagernet.ktx.*
 import io.nekohasekai.sagernet.widget.UndoSnackbarManager
 import libcore.Libcore
-import okhttp3.Request
 import java.io.File
 import java.io.FileNotFoundException
 import java.util.*

+ 12 - 5
app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt

@@ -71,7 +71,6 @@ import kotlinx.coroutines.*
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import libcore.Libcore
-import okhttp3.internal.closeQuietly
 import java.net.InetAddress
 import java.net.InetSocketAddress
 import java.net.Socket
@@ -200,7 +199,9 @@ class ConfigurationFragment @JvmOverloads constructor(
                         RawUpdater.parseRaw(fileText)?.let { pl -> proxies.addAll(pl) }
                         zip.closeEntry()
                     }
-                    zip.closeQuietly()
+                    runCatching {
+                        zip.close()
+                    }
                 } else {
                     val fileText = requireContext().contentResolver.openInputStream(file)!!.use {
                         it.bufferedReader().readText()
@@ -703,7 +704,9 @@ class ConfigurationFragment @JvmOverloads constructor(
                                     profile.ping = (SystemClock.elapsedRealtime() - start).toInt()
                                     test.update(profile)
                                 } finally {
-                                    socket.closeQuietly()
+                                    runCatching {
+                                        socket.close()
+                                    }
                                 }
                             }
                         } catch (e: Exception) {
@@ -814,14 +817,18 @@ class ConfigurationFragment @JvmOverloads constructor(
             }
 
             testJobs.joinAll()
-            dnsInstance.closeQuietly()
+            runCatching {
+                dnsInstance.close()
+            }
             onMainDispatcher {
                 test.binding.progressCircular.isGone = true
                 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setText(android.R.string.ok)
             }
         }
         test.cancel = {
-            dnsInstance.closeQuietly()
+            runCatching {
+                dnsInstance.close()
+            }
             mainJob.cancel()
             runOnDefaultDispatcher {
                 GroupManager.postReload(DataStore.currentGroupId())

+ 0 - 3
app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt

@@ -22,13 +22,10 @@ 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.Logs
 import io.nekohasekai.sagernet.utils.cf.DeviceResponse
 import io.nekohasekai.sagernet.utils.cf.RegisterRequest
 import io.nekohasekai.sagernet.utils.cf.UpdateDeviceRequest
 import libcore.Libcore
-import okhttp3.OkHttp
-import okhttp3.Request
 
 // kang from wgcf
 object Cloudflare {

+ 9 - 5
app/src/main/java/io/nekohasekai/sagernet/widget/LinkOrContentPreference.kt

@@ -28,7 +28,7 @@ import com.takisoft.preferencex.EditTextPreference
 import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.ktx.app
 import io.nekohasekai.sagernet.ktx.readableMessage
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import libcore.Libcore
 
 class LinkOrContentPreference : EditTextPreference {
 
@@ -56,14 +56,18 @@ class LinkOrContentPreference : EditTextPreference {
                 }
 
                 try {
-                    if (Uri.parse(link.toString()).scheme == "content") {
+                    val uri = Uri.parse(link.toString())
+
+                    if (uri.scheme.isNullOrBlank()) {
+                        error("Missing scheme in url")
+                    } else if (uri.scheme == "content") {
                         linkLayout.isErrorEnabled = false
                         return
-                    }
-                    val url = link.toString().toHttpUrl()
-                    if ("http".equals(url.scheme, true)) {
+                    } else if (uri.scheme == "http") {
                         linkLayout.error = app.getString(R.string.cleartext_http_warning)
                         linkLayout.isErrorEnabled = true
+                    } else if (uri.scheme != "https") {
+                        error("Invalid scheme ${uri.scheme}")
                     } else {
                         linkLayout.isErrorEnabled = false
                     }

+ 10 - 4
app/src/main/java/io/nekohasekai/sagernet/widget/LinkPreference.kt

@@ -20,6 +20,7 @@
 package io.nekohasekai.sagernet.widget
 
 import android.content.Context
+import android.net.Uri
 import android.util.AttributeSet
 import androidx.core.widget.addTextChangedListener
 import com.google.android.material.textfield.TextInputLayout
@@ -27,7 +28,7 @@ import com.takisoft.preferencex.EditTextPreference
 import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.ktx.app
 import io.nekohasekai.sagernet.ktx.readableMessage
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import libcore.Libcore
 
 class LinkPreference : EditTextPreference {
 
@@ -78,10 +79,15 @@ class LinkPreference : EditTextPreference {
                     return
                 }
                 try {
-                    val url = link.toString().toHttpUrl()
-                    if ("http".equals(url.scheme, true)) {
+                    val uri = Uri.parse(link.toString())
+
+                    if (uri.scheme.isNullOrBlank()) {
+                        error("Missing scheme in url")
+                    } else if (uri.scheme == "http") {
                         linkLayout.error = app.getString(R.string.cleartext_http_warning)
                         linkLayout.isErrorEnabled = true
+                    } else if (uri.scheme != "https") {
+                        error("Invalid scheme ${uri.scheme}")
                     } else {
                         linkLayout.isErrorEnabled = false
                     }
@@ -101,7 +107,7 @@ class LinkPreference : EditTextPreference {
                 text = defaultValue
                 false
             } else try {
-                newValue.toHttpUrl()
+                Libcore.parseURL(newValue)
                 true
             } catch (ignored: Exception) {
                 false

+ 5 - 6
app/src/main/java/io/nekohasekai/sagernet/widget/OOCv1TokenPreference.kt

@@ -28,7 +28,8 @@ import com.google.android.material.textfield.TextInputLayout
 import com.takisoft.preferencex.EditTextPreference
 import io.nekohasekai.sagernet.R
 import io.nekohasekai.sagernet.ktx.readableMessage
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import libcore.Libcore
+import java.net.URL
 
 class OOCv1TokenPreference : EditTextPreference {
 
@@ -83,7 +84,7 @@ class OOCv1TokenPreference : EditTextPreference {
                                 linkLayout.error = "Protocol scheme must be https"
                             }
                             else -> try {
-                                baseUrl.toHttpUrl()
+                                Libcore.parseURL(baseUrl)
                             } catch (e: Exception) {
                                 isValid = false
                                 linkLayout.error = e.readableMessage
@@ -104,13 +105,11 @@ class OOCv1TokenPreference : EditTextPreference {
                             when {
                                 certSha256.length != 64 -> {
                                     isValid = false
-                                    linkLayout.error =
-                                        "certSha256 must be a SHA-256 hexadecimal string"
+                                    linkLayout.error = "certSha256 must be a SHA-256 hexadecimal string"
                                 }
                                 !certSha256.all { CharUtil.isLetterLower(it) || CharUtil.isNumber(it) } -> {
                                     isValid = false
-                                    linkLayout.error =
-                                        "certSha256 must be a hexadecimal string with lowercase letters"
+                                    linkLayout.error = "certSha256 must be a hexadecimal string with lowercase letters"
                                 }
                             }
                         }

+ 0 - 1
buildSrc/src/main/kotlin/Helpers.kt

@@ -125,7 +125,6 @@ fun Project.setupCommon() {
                     "org/**",
                     "**/*.java",
                     "**/*.proto",
-                    "okhttp3/**"
                 )
             )
         }