@@ -2,43 +2,59 @@ package com.lagradost.cloudstream3.network
2
2
3
3
import android.util.Log
4
4
import okhttp3.Credentials
5
+ import okhttp3.Dns
6
+ import okhttp3.HttpUrl.Companion.toHttpUrl
5
7
import okhttp3.Interceptor
6
8
import okhttp3.OkHttpClient
7
9
import okhttp3.Response
10
+ import okhttp3.dnsoverhttps.DnsOverHttps
11
+ import org.xbill.DNS.*
8
12
import java.io.IOException
9
13
import java.net.ConnectException
14
+ import java.net.InetAddress
10
15
import java.net.InetSocketAddress
11
16
import java.net.Proxy
12
17
import java.net.SocketTimeoutException
18
+ import java.net.UnknownHostException
13
19
import java.util.concurrent.TimeUnit
14
20
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
+ */
15
34
class ProxyInterceptor (
16
35
private val host : String ,
17
36
private val port : Int ,
18
37
private val proxyType : Proxy .Type = Proxy .Type .HTTP ,
19
38
private val username : String? = null ,
20
39
private val password : String? = null ,
21
40
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
24
44
) : Interceptor {
25
45
26
46
companion object {
27
47
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
+ )
28
54
}
29
55
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
42
58
}
43
59
44
60
private val proxyClient by lazy {
@@ -47,8 +63,9 @@ class ProxyInterceptor(
47
63
val proxy = Proxy (proxyType, InetSocketAddress (host, port))
48
64
OkHttpClient .Builder ()
49
65
.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 )
52
69
.apply {
53
70
if (username != null && password != null ) {
54
71
Log .d(TAG , " Configuring proxy credentials" )
@@ -63,6 +80,79 @@ class ProxyInterceptor(
63
80
.build()
64
81
}
65
82
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
+ */
66
156
override fun intercept (chain : Interceptor .Chain ): Response {
67
157
Log .d(TAG , " Intercepting request to ${chain.request().url.host} " )
68
158
@@ -71,11 +161,11 @@ class ProxyInterceptor(
71
161
72
162
Log .d(
73
163
TAG ,
74
- " proxy response:" + listOf (
164
+ " Proxy response:" + listOf (
75
165
" url=${response.request.url} " ,
76
166
" status=${response.code} " ,
77
167
" headers=${response.headers.size} " ,
78
- " body=${response.body? .contentLength() ? : 0 } bytes"
168
+ " body=${response.body.contentLength()} bytes"
79
169
).joinToString(separator = " , " )
80
170
)
81
171
@@ -87,8 +177,8 @@ class ProxyInterceptor(
87
177
} catch (e: IOException ) {
88
178
Log .d(
89
179
TAG ,
90
- " proxy error:" + listOf (
91
- " type=${e.javaClass.simpleName } " ,
180
+ " Proxy error:" + listOf (
181
+ " type=${e.javaClass} " ,
92
182
" message=${e.message} " ,
93
183
" request=${chain.request().url} "
94
184
).joinToString(separator = " , " )
@@ -119,12 +209,12 @@ class ProxyInterceptor(
119
209
}
120
210
121
211
is SocketTimeoutException -> {
122
- Log .d(TAG , " Timeout connecting to proxy (${connectTimeoutSeconds } s)" )
212
+ Log .d(TAG , " Timeout connecting to proxy (${connectTimeoutMillis } s)" )
123
213
if (allowFallback) fallback(chain) else throw e
124
214
}
125
215
126
216
else -> {
127
- Log .d(TAG , " Unexpected proxy error: ${e.javaClass.simpleName } " )
217
+ Log .d(TAG , " Unexpected proxy error: ${e.javaClass} " )
128
218
throw e
129
219
}
130
220
}
@@ -135,12 +225,38 @@ class ProxyInterceptor(
135
225
return chain.proceed(chain.request()).also { response ->
136
226
Log .d(
137
227
TAG ,
138
- " direct connection: " + listOf (
228
+ " Direct connection: " + listOf (
139
229
" status=${response.code} " ,
140
230
" via=${response.handshake?.tlsVersion ? : " Plaintext" } " ,
141
231
" server=${response.header(" Server" ) ? : " Unknown" } "
142
232
).joinToString(separator = " , " )
143
233
)
144
234
}
145
235
}
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
+ }
146
262
}
0 commit comments