diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt index 90fb96b3..e8758012 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/AngConfigManager.kt @@ -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) "" diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt index b61c0258..de8f154b 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SettingsManager.kt @@ -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) } /** diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt index 2d9639ee..3f5eb400 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/SpeedtestManager.kt @@ -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 { - 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( diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt index f4dd062d..2e769c7d 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/UpdateCheckerManager.kt @@ -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(".") diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt index eeac9b6f..d83b8904 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/handler/V2rayConfigManager.kt @@ -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) } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt index 088e1d9a..460de6b4 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt @@ -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) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index c2f919db..80c67439 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -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)) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt index 863852db..74bc051e 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/util/HttpUtil.kt @@ -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 + } + } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/UserAssetViewModel.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/UserAssetViewModel.kt index 902e325b..3799cd52 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/UserAssetViewModel.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/viewmodel/UserAssetViewModel.kt @@ -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() @@ -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() @@ -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 }