@@ -2,43 +2,59 @@ package com.lagradost.cloudstream3.network
22
33import android.util.Log
44import okhttp3.Credentials
5+ import okhttp3.Dns
6+ import okhttp3.HttpUrl.Companion.toHttpUrl
57import okhttp3.Interceptor
68import okhttp3.OkHttpClient
79import okhttp3.Response
10+ import okhttp3.dnsoverhttps.DnsOverHttps
11+ import org.xbill.DNS.*
812import java.io.IOException
913import java.net.ConnectException
14+ import java.net.InetAddress
1015import java.net.InetSocketAddress
1116import java.net.Proxy
1217import java.net.SocketTimeoutException
18+ import java.net.UnknownHostException
1319import java.util.concurrent.TimeUnit
1420
21+ /* *
22+ * An OkHttp Interceptor that routes requests through a proxy with custom DNS resolution.
23+ *
24+ * @param host The proxy server hostname or IP address.
25+ * @param port The proxy server port.
26+ * @param proxyType The type of proxy (e.g., HTTP, SOCKS). Defaults to HTTP.
27+ * @param username Optional proxy username for authentication.
28+ * @param password Optional proxy password for authentication.
29+ * @param allowFallback Whether to fall back to a direct connection if the proxy fails. Defaults to false.
30+ * @param connectTimeoutMillis Connection timeout in seconds. Defaults to 15.
31+ * @param readTimeoutMillis Read timeout in seconds. Defaults to 15.
32+ * @param dnsServer Optional custom DNS server (e.g., "8.8.8.8" or "cloudflare" for DoH).
33+ */
1534class ProxyInterceptor (
1635 private val host : String ,
1736 private val port : Int ,
1837 private val proxyType : Proxy .Type = Proxy .Type .HTTP ,
1938 private val username : String? = null ,
2039 private val password : String? = null ,
2140 private val allowFallback : Boolean = false ,
22- private val connectTimeoutSeconds : Long = 15L ,
23- private val readTimeoutSeconds : Long = 15L
41+ private val connectTimeoutMillis : Long = 15_000L ,
42+ private val readTimeoutMillis : Long = 15_000L ,
43+ private val dnsServer : String? = null
2444) : Interceptor {
2545
2646 companion object {
2747 private const val TAG = " ProxyDebug"
48+ private val DNS_OVER_HTTPS_URLS = mapOf (
49+ " cloudflare" to " https://cloudflare-dns.com/dns-query" ,
50+ " google" to " https://dns.google/dns-query" ,
51+ " quad9" to " https://dns.quad9.net/dns-query" ,
52+ " adguard" to " https://dns.adguard.com/dns-query"
53+ )
2854 }
2955
30- init {
31- Log .d(
32- TAG ,
33- " proxy setup: " + listOf (
34- " host=$host " ,
35- " port=$port " ,
36- " type=${proxyType.name} " ,
37- " timeouts=${connectTimeoutSeconds} s/${readTimeoutSeconds} s" ,
38- " auth=${if (username != null ) " enabled" else " None" } " ,
39- " fallback=${if (allowFallback) " Allowed" else " Disabled" } "
40- ).joinToString(separator = " , " ) // Join only the parameters
41- )
56+ private val internalDns by lazy {
57+ dnsServer?.let { createDnsResolver(it) } ? : Dns .SYSTEM
4258 }
4359
4460 private val proxyClient by lazy {
@@ -47,8 +63,9 @@ class ProxyInterceptor(
4763 val proxy = Proxy (proxyType, InetSocketAddress (host, port))
4864 OkHttpClient .Builder ()
4965 .proxy(proxy)
50- .connectTimeout(connectTimeoutSeconds, TimeUnit .SECONDS )
51- .readTimeout(readTimeoutSeconds, TimeUnit .SECONDS )
66+ .dns(internalDns)
67+ .connectTimeout(connectTimeoutMillis, TimeUnit .MILLISECONDS )
68+ .readTimeout(readTimeoutMillis, TimeUnit .MILLISECONDS )
5269 .apply {
5370 if (username != null && password != null ) {
5471 Log .d(TAG , " Configuring proxy credentials" )
@@ -63,6 +80,79 @@ class ProxyInterceptor(
6380 .build()
6481 }
6582
83+ /* *
84+ * Creates a custom DNS resolver based on the provided server.
85+ *
86+ * @param server The DNS server (e.g., DoH keyword, DoH URL, or IP address).
87+ * @return A configured Dns instance.
88+ */
89+ private fun createDnsResolver (server : String ): Dns {
90+ return when {
91+ server in DNS_OVER_HTTPS_URLS -> {
92+ val url = DNS_OVER_HTTPS_URLS .getValue(server)
93+ DnsOverHttps .Builder ()
94+ .client(OkHttpClient ())
95+ .url(url.toHttpUrl())
96+ .build()
97+ }
98+
99+ server.startsWith(" https://" ) -> {
100+ try {
101+ DnsOverHttps .Builder ()
102+ .client(OkHttpClient ())
103+ .url(server.toHttpUrl())
104+ .build()
105+ } catch (e: IllegalArgumentException ) {
106+ Log .e(TAG , " Invalid DoH URL: $server " )
107+ Dns .SYSTEM
108+ }
109+ }
110+
111+ else -> {
112+ Log .d(TAG , " Using dnsjava for custom DNS server: $server " )
113+ val resolver = SimpleResolver (server)
114+ val cacheA = Lookup .getDefaultCache(Type .A )
115+ val cacheAAAA = Lookup .getDefaultCache(Type .AAAA )
116+
117+ Dns { hostname ->
118+ try {
119+ val lookupA = Lookup (hostname, Type .A )
120+ lookupA.setResolver(resolver)
121+ lookupA.setCache(cacheA)
122+ val aRecords =
123+ lookupA.run ()?.map { InetAddress .getByName(hostname) } ? : emptyList()
124+
125+ val lookupAAAA = Lookup (hostname, Type .AAAA )
126+ lookupAAAA.setResolver(resolver)
127+ lookupAAAA.setCache(cacheAAAA)
128+ val aaaaRecords =
129+ lookupAAAA.run ()?.map { InetAddress .getByName(hostname) }
130+ ? : emptyList()
131+
132+ (aRecords + aaaaRecords).ifEmpty {
133+ throw IOException (" No DNS records found for $hostname " )
134+ }
135+ } catch (e: UnknownHostException ) {
136+ Log .w(TAG , " DNS lookup failed for $hostname : ${e.message} " )
137+ Dns .SYSTEM .lookup(hostname)
138+ } catch (e: IOException ) {
139+ Log .w(TAG , " IO error during DNS lookup for $hostname : ${e.message} " )
140+ Dns .SYSTEM .lookup(hostname)
141+ } catch (e: Exception ) {
142+ Log .e(TAG , " Unexpected error during DNS lookup for $hostname " , e)
143+ throw e // Rethrow unexpected errors
144+ }
145+ }
146+ }
147+ }
148+ }
149+
150+ /* *
151+ * Intercepts the request and routes it through the proxy.
152+ *
153+ * @param chain The interceptor chain.
154+ * @return The response from the proxy or fallback.
155+ */
66156 override fun intercept (chain : Interceptor .Chain ): Response {
67157 Log .d(TAG , " Intercepting request to ${chain.request().url.host} " )
68158
@@ -71,11 +161,11 @@ class ProxyInterceptor(
71161
72162 Log .d(
73163 TAG ,
74- " proxy response:" + listOf (
164+ " Proxy response:" + listOf (
75165 " url=${response.request.url} " ,
76166 " status=${response.code} " ,
77167 " headers=${response.headers.size} " ,
78- " body=${response.body? .contentLength() ? : 0 } bytes"
168+ " body=${response.body.contentLength()} bytes"
79169 ).joinToString(separator = " , " )
80170 )
81171
@@ -87,8 +177,8 @@ class ProxyInterceptor(
87177 } catch (e: IOException ) {
88178 Log .d(
89179 TAG ,
90- " proxy error:" + listOf (
91- " type=${e.javaClass.simpleName } " ,
180+ " Proxy error:" + listOf (
181+ " type=${e.javaClass} " ,
92182 " message=${e.message} " ,
93183 " request=${chain.request().url} "
94184 ).joinToString(separator = " , " )
@@ -119,12 +209,12 @@ class ProxyInterceptor(
119209 }
120210
121211 is SocketTimeoutException -> {
122- Log .d(TAG , " Timeout connecting to proxy (${connectTimeoutSeconds } s)" )
212+ Log .d(TAG , " Timeout connecting to proxy (${connectTimeoutMillis } s)" )
123213 if (allowFallback) fallback(chain) else throw e
124214 }
125215
126216 else -> {
127- Log .d(TAG , " Unexpected proxy error: ${e.javaClass.simpleName } " )
217+ Log .d(TAG , " Unexpected proxy error: ${e.javaClass} " )
128218 throw e
129219 }
130220 }
@@ -135,12 +225,38 @@ class ProxyInterceptor(
135225 return chain.proceed(chain.request()).also { response ->
136226 Log .d(
137227 TAG ,
138- " direct connection: " + listOf (
228+ " Direct connection: " + listOf (
139229 " status=${response.code} " ,
140230 " via=${response.handshake?.tlsVersion ? : " Plaintext" } " ,
141231 " server=${response.header(" Server" ) ? : " Unknown" } "
142232 ).joinToString(separator = " , " )
143233 )
144234 }
145235 }
236+
237+ /* *
238+ * Tests the DNS configuration by resolving "example.com".
239+ *
240+ * @return True if DNS resolution succeeds, false otherwise.
241+ */
242+ fun testDnsConfiguration (): Boolean {
243+ val testDomain = " example.com"
244+ Log .d(TAG , " Testing DNS resolution for $testDomain " )
245+ return try {
246+ val addresses = internalDns.lookup(testDomain)
247+ if (addresses.isNotEmpty()) {
248+ Log .d(
249+ TAG ,
250+ " DNS resolution successful: ${addresses.joinToString { it.hostAddress ? : " unknown" }} "
251+ )
252+ true
253+ } else {
254+ Log .w(TAG , " DNS resolution returned no addresses for $testDomain " )
255+ false
256+ }
257+ } catch (e: Exception ) {
258+ Log .e(TAG , " DNS test failed: ${e.javaClass} - ${e.message} " )
259+ false
260+ }
261+ }
146262}
0 commit comments