Use OkHttp with proxy auth and support SOCKS creds

Replace HttpURLConnection-based networking with OkHttp and add proxy username/password support. Introduces buildOkHttpClient, updated getUrlContent/getUrlContentWithUserAgent signatures, and downloadToFile; callers in AngConfigManager, SpeedtestManager, UpdateCheckerManager, PerAppProxyActivity, UserAssetActivity and UserAssetViewModel now pass SOCKS proxy credentials. Removes createProxyConnection and related URL-resolve logic, clears HTTP inbound auth/udp in V2rayConfigManager, and simplifies SettingsManager port generation. These changes enable authenticated proxy requests, improved redirect handling and more robust file downloads.
This commit is contained in:
2dust committed 2026-04-23 20:40:17 +08:00
1 parent 8be8a912c4
commit 4199cee4e5
9 files changed
+166 -187

No files matched your search

@@ -532,10 +532,12 @@ object AngConfigManager {
}
LogUtil.i(AppConfig.TAG, url)
val userAgent = it.subscription.userAgent
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var configText = try {
val httpPort = SettingsManager.getHttpPort()
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort)
HttpUtil.getUrlContentWithUserAgent(url, userAgent, 15000, httpPort, proxyUsername, proxyPassword)
} catch (e: Exception) {
LogUtil.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
""
@@ -317,11 +317,7 @@ object SettingsManager {
}
private fun generateRandomSocksPort(): Int {
return if (Utils.isXray()) {
Random.nextInt(10000, 65536)
} else {
Random.nextInt(10000, 65535)
}
return Random.nextInt(10000, 65535)
}
/**
@@ -83,49 +83,14 @@ object SpeedtestManager {
}
}
/**
* Tests the connection to a given URL and port.
*
* @param context The Context in which the test is running.
* @param port The port to connect to.
* @return A pair containing the elapsed time in milliseconds and the result message.
*/
fun testConnection(context: Context, port: Int): Pair<Long, String> {
var result: String
var elapsed = -1L
val conn = HttpUtil.createProxyConnection(SettingsManager.getDelayTestUrl(), port, 15000, 15000) ?: return Pair(elapsed, "")
try {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
elapsed = SystemClock.elapsedRealtime() - start
result = when (code) {
204 -> context.getString(R.string.connection_test_available, elapsed)
200 if conn.contentLengthLong == 0L -> context.getString(R.string.connection_test_available, elapsed)
else -> throw IOException(
context.getString(R.string.connection_test_error_status_code, code)
)
}
} catch (e: IOException) {
LogUtil.e(AppConfig.TAG, "Connection test IOException", e)
result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Connection test Exception", e)
result = context.getString(R.string.connection_test_error, e.message)
} finally {
conn.disconnect()
}
return Pair(elapsed, result)
}
fun getRemoteIPInfo(): String? {
val url = MmkvManager.decodeSettingsString(AppConfig.PREF_IP_API_URL)
.takeIf { !it.isNullOrBlank() } ?: AppConfig.IP_API_URL
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
val content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: return null
val content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: return null
val ipInfo = JsonUtil.fromJson(content, IPAPIInfo::class.java) ?: return null
val ip = listOf(
@@ -23,10 +23,13 @@ object UpdateCheckerManager {
AppConfig.APP_API_URL.concatUrl("latest")
}
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
var response = HttpUtil.getUrlContent(url, 5000)
if (response.isNullOrEmpty()) {
val httpPort = SettingsManager.getHttpPort()
response = HttpUtil.getUrlContent(url, 5000, httpPort)
response = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword)
?: throw IllegalStateException("Failed to get response")
}
@@ -61,39 +64,6 @@ object UpdateCheckerManager {
}
}
suspend fun downloadApk(context: Context, downloadUrl: String): File? = withContext(Dispatchers.IO) {
try {
val httpPort = SettingsManager.getHttpPort()
val connection = HttpUtil.createProxyConnection(downloadUrl, httpPort, 10000, 10000, true)
?: throw IllegalStateException("Failed to create connection")
try {
val apkFile = File(context.cacheDir, "update.apk")
LogUtil.i(AppConfig.TAG, "Downloading APK to: ${apkFile.absolutePath}")
FileOutputStream(apkFile).use { outputStream ->
connection.inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
LogUtil.i(AppConfig.TAG, "APK download completed")
return@withContext apkFile
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download APK: ${e.message}")
return@withContext null
} finally {
try {
connection.disconnect()
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Error closing connection: ${e.message}")
}
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to initiate download: ${e.message}")
return@withContext null
}
}
private fun compareVersions(version1: String, version2: String): Int {
val v1 = version1.split(".")
val v2 = version2.split(".")
@@ -416,6 +416,8 @@ object V2rayConfigManager {
inbound2.tag = EConfigType.HTTP.name.lowercase()
inbound2.port = SettingsManager.getHttpPort()
inbound2.protocol = EConfigType.HTTP.name.lowercase()
inbound2.settings?.auth = null
inbound2.settings?.udp = null
v2rayConfig.inbounds.add(inbound2)
}
@@ -191,8 +191,10 @@ class PerAppProxyActivity : BaseActivity() {
lifecycleScope.launch(Dispatchers.IO) {
var content = HttpUtil.getUrlContent(url, 5000)
if (content.isNullOrEmpty()) {
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
content = HttpUtil.getUrlContent(url, 5000, httpPort) ?: ""
content = HttpUtil.getUrlContent(url, 5000, httpPort, proxyUsername, proxyPassword) ?: ""
}
launch(Dispatchers.Main) {
//LogUtil.i(AppConfig.TAG, content)
@@ -173,9 +173,11 @@ class UserAssetActivity : HelperBaseActivity() {
showLoading()
toast(R.string.msg_downloading_content)
val proxyUsername = SettingsManager.getSocksUsername()
val proxyPassword = SettingsManager.getSocksPassword()
val httpPort = SettingsManager.getHttpPort()
lifecycleScope.launch(Dispatchers.IO) {
val result = viewModel.downloadGeoFiles(extDir, httpPort)
val result = viewModel.downloadGeoFiles(extDir, httpPort, proxyUsername, proxyPassword)
withContext(Dispatchers.Main) {
if (result.successCount > 0) {
toast(getString(R.string.title_update_config_count, result.successCount))
@@ -3,10 +3,8 @@ package com.v2ray.ang.util
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.LOOPBACK
import com.v2ray.ang.BuildConfig
import com.v2ray.ang.util.Utils.encode
import com.v2ray.ang.util.Utils.urlDecode
import java.io.File
import java.io.IOException
import java.net.HttpURLConnection
import java.net.IDN
import java.net.Inet6Address
import java.net.InetAddress
@@ -15,6 +13,10 @@ import java.net.MalformedURLException
import java.net.Proxy
import java.net.URI
import java.net.URL
import java.util.concurrent.TimeUnit
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request
object HttpUtil {
@@ -106,13 +108,31 @@ object HttpUtil {
* @param httpPort The HTTP port to use.
* @return The content of the URL as a string.
*/
fun getUrlContent(url: String, timeout: Int, httpPort: Int = 0): String? {
val conn = createProxyConnection(url, httpPort, timeout, timeout) ?: return null
fun getUrlContent(
url: String,
timeout: Int,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String? {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
try {
return conn.inputStream.bufferedReader().readText()
} catch (_: Exception) {
} finally {
conn.disconnect()
client.newCall(requestBuilder.build()).execute().use { response ->
if (!response.isSuccessful) {
LogUtil.w(AppConfig.TAG, "Failed to get URL content, code=${response.code}")
return null
}
return response.body?.string()
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to get URL content", e)
}
return null
}
@@ -127,127 +147,147 @@ object HttpUtil {
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun getUrlContentWithUserAgent(url: String?, userAgent: String?, timeout: Int = 15000, httpPort: Int = 0): String {
fun getUrlContentWithUserAgent(
url: String?,
userAgent: String?,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): String {
var currentUrl = url
var redirects = 0
val maxRedirects = 3
while (redirects++ < maxRedirects) {
if (currentUrl == null) continue
val conn = createProxyConnection(currentUrl, httpPort, timeout, timeout) ?: continue
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = false)
val finalUserAgent = if (userAgent.isNullOrBlank()) {
"v2rayNG/${BuildConfig.VERSION_NAME}"
} else {
userAgent
}
conn.setRequestProperty("User-agent", finalUserAgent)
conn.connect()
val requestBuilder = Request.Builder()
.url(currentUrl)
.get()
.header("User-agent", finalUserAgent)
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
val responseCode = conn.responseCode
when (responseCode) {
in 300..399 -> {
val location = resolveLocation(conn)
conn.disconnect()
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
client.newCall(requestBuilder.build()).execute().use { response ->
when {
response.isRedirect -> {
val location = response.header("Location")
if (location.isNullOrEmpty()) {
throw IOException("Redirect location not found")
}
currentUrl = resolveLocation(currentUrl, location)
if (currentUrl.isNullOrEmpty()) {
throw IOException("Failed to resolve redirect location")
}
continue
}
currentUrl = location
continue
}
else -> try {
return conn.inputStream.use { it.bufferedReader().readText() }
} finally {
conn.disconnect()
response.isSuccessful -> {
return response.body?.string() ?: ""
}
else -> {
throw IOException("Request failed with status code ${response.code}")
}
}
}
}
throw IOException("Too many redirects")
}
/**
* Creates an HttpURLConnection object connected through a proxy.
*
* @param urlStr The target URL address.
* @param port The port of the proxy server.
* @param connectTimeout The connection timeout in milliseconds (default is 15000 ms).
* @param readTimeout The read timeout in milliseconds (default is 15000 ms).
* @param needStream Whether the connection needs to support streaming.
* @return Returns a configured HttpURLConnection object, or null if it fails.
*/
fun createProxyConnection(
urlStr: String,
port: Int,
connectTimeout: Int = 15000,
readTimeout: Int = 15000,
needStream: Boolean = false
): HttpURLConnection? {
private fun buildOkHttpClient(
timeout: Int,
httpPort: Int,
proxyUsername: String?,
proxyPassword: String?,
followRedirects: Boolean
): OkHttpClient {
val builder = OkHttpClient.Builder()
.connectTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
.readTimeout(timeout.toLong(), TimeUnit.MILLISECONDS)
.followRedirects(followRedirects)
.followSslRedirects(followRedirects)
var conn: HttpURLConnection? = null
try {
val url = URL(urlStr)
// Create a connection
conn = if (port == 0) {
url.openConnection()
} else {
url.openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress(LOOPBACK, port)
)
)
} as HttpURLConnection
// Set connection and read timeouts
conn.connectTimeout = connectTimeout
conn.readTimeout = readTimeout
if (!needStream) {
// Set request headers
conn.setRequestProperty("Connection", "close")
// Disable automatic redirects
conn.instanceFollowRedirects = false
// Disable caching
conn.useCaches = false
if (httpPort != 0) {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(LOOPBACK, httpPort)))
if (!proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
builder.proxyAuthenticator { _, response ->
if (response.request.header("Proxy-Authorization") != null) {
null
} else {
response.request.newBuilder()
.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
.build()
}
}
}
//Add Basic Authorization
url.userInfo?.let {
conn.setRequestProperty(
"Authorization",
"Basic ${encode(urlDecode(it))}"
)
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to create proxy connection", e)
// If an exception occurs, close the connection and return null
conn?.disconnect()
return null
}
return conn
return builder.build()
}
// Returns absolute URL string location header sets
fun resolveLocation(conn: HttpURLConnection): String? {
val raw = conn.getHeaderField("Location")?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// Try check url is relative or absolute
private fun resolveLocation(baseUrl: String, raw: String): String? {
return try {
val locUri = URI(raw)
val baseUri = conn.url.toURI()
val baseUri = URI(baseUrl)
val resolved = if (locUri.isAbsolute) locUri else baseUri.resolve(locUri)
resolved.toURL().toString()
} catch (_: Exception) {
// Fallback: url resolver, also should handles //host/...
try {
URL(raw).toString() // absolute with protocol
URL(raw).toString()
} catch (_: MalformedURLException) {
try {
URL(conn.url, raw).toString()
URL(URL(baseUrl), raw).toString()
} catch (_: MalformedURLException) {
null
}
}
}
}
fun downloadToFile(
url: String,
targetFile: File,
timeout: Int = 15000,
httpPort: Int = 0,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean {
val client = buildOkHttpClient(timeout, httpPort, proxyUsername, proxyPassword, followRedirects = true)
val requestBuilder = Request.Builder()
.url(url)
.get()
.header("Connection", "close")
if (httpPort != 0 && !proxyUsername.isNullOrBlank() && !proxyPassword.isNullOrBlank()) {
requestBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword))
}
return try {
client.newCall(requestBuilder.build()).execute().use { response ->
if (!response.isSuccessful) {
LogUtil.w(AppConfig.TAG, "Failed to download file, code=${response.code}, url=$url")
return false
}
val body = response.body ?: return false
body.byteStream().use { input ->
targetFile.outputStream().use { output ->
input.copyTo(output)
}
}
true
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download file: $url", e)
false
}
}
}
@@ -10,8 +10,6 @@ import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.Utils
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
class UserAssetViewModel : ViewModel() {
private val assets = mutableListOf<AssetUrlCache>()
@@ -61,7 +59,12 @@ class UserAssetViewModel : ViewModel() {
}
}
fun downloadGeoFiles(extDir: File, httpPort: Int): GeoDownloadResult {
fun downloadGeoFiles(
extDir: File,
httpPort: Int,
proxyUsername: String? = null,
proxyPassword: String? = null
): GeoDownloadResult {
val snapshot = getAssets()
var successCount = 0
val failures = mutableListOf<String>()
@@ -69,7 +72,7 @@ class UserAssetViewModel : ViewModel() {
snapshot.forEach { cache ->
val item = cache.assetUrl
val portsToTry = if (httpPort == 0) listOf(0) else listOf(httpPort, 0)
if (portsToTry.any { tryDownload(item, extDir, it) }) {
if (portsToTry.any { tryDownload(item, extDir, it, proxyUsername, proxyPassword) }) {
successCount++
} else {
failures.add(item.remarks)
@@ -79,25 +82,22 @@ class UserAssetViewModel : ViewModel() {
return GeoDownloadResult(successCount, failures.size, failures)
}
private fun tryDownload(item: AssetUrlItem, extDir: File, httpPort: Int): Boolean {
private fun tryDownload(
item: AssetUrlItem,
extDir: File,
httpPort: Int,
proxyUsername: String? = null,
proxyPassword: String? = null
): Boolean {
val targetTemp = File(extDir, item.remarks + "_temp")
val target = File(extDir, item.remarks)
val conn = HttpUtil.createProxyConnection(item.url, httpPort, 15000, 15000, needStream = true) ?: return false
try {
val responseCode = conn.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
conn.inputStream.use { inputStream ->
FileOutputStream(targetTemp).use { output ->
inputStream.copyTo(output)
}
}
if (HttpUtil.downloadToFile(item.url, targetTemp, 15000, httpPort, proxyUsername, proxyPassword)) {
targetTemp.renameTo(target)
return true
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Failed to download geo file: ${item.remarks}", e)
} finally {
conn.disconnect()
}
return false
}