diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index a9cd9c01edd..1a5ad05d3ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -84,6 +84,9 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { } AppDebug.isDebug = BuildConfig.DEBUG + + // Auto-restore VPN connection in the background if enabled + com.lagradost.cloudstream3.network.initializeVpn(this) } override fun attachBaseContext(base: Context?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index 203a503e903..2bb84ac4034 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -1,10 +1,12 @@ package com.lagradost.cloudstream3.network import android.content.Context +import android.util.Log import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ignoreAllSSLErrors @@ -65,6 +67,21 @@ fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClie 8 -> addCanadianShieldDns() } } + .apply { + val vpnPref = settingsManager.getString(context.getString(R.string.vpn_country_pref_key), "NONE") ?: "NONE" + if (vpnPref != "NONE") { + val server = currentVpnServer + if (server != null) { + Log.d("RequestsHelper", "VPN_DEBUG buildDefaultClient: applying proxy ${server.host}:${server.proxyPort} [${server.proxyType}] insecure=${vpnAllowInsecure}") + addVpnProxy(server) + // When insecure mode is on and VPN is active, ignore SSL errors + // so MITM-style proxies can forward HTTPS traffic without cert validation. + if (vpnAllowInsecure) ignoreAllSSLErrors() + } else { + Log.d("RequestsHelper", "VPN_DEBUG buildDefaultClient: vpnPref=$vpnPref but currentVpnServer=null — proxy NOT applied") + } + } + } // Needs to be build as otherwise the other builders will change this object .build() return baseClient diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/VpnProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/network/VpnProviders.kt new file mode 100644 index 00000000000..9a78e961359 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/network/VpnProviders.kt @@ -0,0 +1,373 @@ +package com.lagradost.cloudstream3.network + +import android.util.Log +import com.lagradost.cloudstream3.mvvm.logError +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONArray +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit +import com.lagradost.nicehttp.ignoreAllSSLErrors +import android.content.Context +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.mapper +import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import com.lagradost.cloudstream3.R + +private const val TAG = "VpnProviders" + +// ───────────────────────────────────────────────────────────────────────────── +// Data model +// ───────────────────────────────────────────────────────────────────────────── + +enum class VpnSource { NONE, PROXIFLY } + +data class VpnServer( + val name: String, + val host: String, + val port: Int, + val source: VpnSource, + /** Confirmed working proxy port (set after testProxyWithOkHttp passes). */ + val proxyPort: Int = port, + /** Confirmed working proxy protocol (set after testProxyWithOkHttp passes). */ + val proxyType: Proxy.Type = Proxy.Type.SOCKS, + val username: String? = null, + val password: String? = null, +) + +// ───────────────────────────────────────────────────────────────────────────── +// Proxifly SOCKS5 data source +// +// Raw JSON URL (updated every ~10 min by Proxifly CI): +// https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/protocols/socks5/data.json +// +// JSON structure per entry: +// { "ip":"1.2.3.4", "port":1080, "score":5, "geolocation":{"country":"US"} } +// +// Country filtering is done client-side (no per-country URL available). +// ───────────────────────────────────────────────────────────────────────────── + +private const val PROXIFLY_SOCKS5_URL = + "https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/protocols/socks5/data.json" + +/** Cache TTL: re-fetch the list after 1 hour. */ +private const val PROXY_LIST_TTL_MS = 60 * 60 * 1_000L + +private var cachedProxyList: List>? = null // host, port, countryCode +private var proxyCachedAt: Long = 0L + +/** + * Fetches and caches the full Proxifly SOCKS5 list. + * Each entry is (ip, port, countryCode). + * Results are sorted descending by score so high-quality proxies are tried first. + */ +private suspend fun fetchProxiflyList(): List> = + withContext(Dispatchers.IO) { + val now = System.currentTimeMillis() + val cached = cachedProxyList + if (cached != null && now - proxyCachedAt < PROXY_LIST_TTL_MS) { + return@withContext cached + } + try { + val client = com.lagradost.cloudstream3.app.baseClient.newBuilder() + .proxy(Proxy.NO_PROXY) + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + + val request = Request.Builder() + .url(PROXIFLY_SOCKS5_URL) + .header("User-Agent", "Mozilla/5.0") + .build() + + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + Log.w(TAG, "Proxifly returned code ${response.code}") + return@withContext cachedProxyList ?: emptyList() + } + val json = response.body?.string() ?: "" + val arr = JSONArray(json) + + data class Entry(val ip: String, val port: Int, val country: String, val score: Int) + + val entries = mutableListOf() + for (i in 0 until arr.length()) { + val obj = arr.getJSONObject(i) + val ip = obj.optString("ip", "") + val port = obj.optInt("port", 0) + val score = obj.optInt("score", 0) + val country = obj.optJSONObject("geolocation")?.optString("country", "") ?: "" + if (ip.isNotEmpty() && port > 0) { + entries += Entry(ip, port, country.uppercase(), score) + } + } + + val sorted = entries.sortedByDescending { it.score } + .map { Triple(it.ip, it.port, it.country) } + + Log.d(TAG, "Fetched ${sorted.size} Proxifly SOCKS5 entries") + cachedProxyList = sorted + proxyCachedAt = now + sorted + } catch (e: Exception) { + logError(e) + Log.w(TAG, "Failed to fetch Proxifly list: ${e.message}") + cachedProxyList ?: emptyList() + } + } + +/** + * Returns all SOCKS5 server candidates from Proxifly, + * optionally filtered by [countryCode] (ISO 3166-1 alpha-2, e.g. "US"). + * Pass null to get the best proxies from any country. + */ +suspend fun fetchProxiflyServers(countryCode: String? = null): List { + val list = fetchProxiflyList() + val filtered = if (countryCode == null) list + else list.filter { it.third.equals(countryCode, ignoreCase = true) } + + val label = countryCode ?: "Any" + return filtered.map { (ip, port, cc) -> + VpnServer( + name = "SOCKS5 \u2013 $label ($ip)", + host = ip, + port = port, + source = VpnSource.PROXIFLY, + proxyPort = port, + proxyType = Proxy.Type.SOCKS, + ) + }.also { + Log.d(TAG, "Candidates for country=$label: ${it.size}") + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// OkHttp integration +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Applies a SOCKS5 proxy to this [OkHttpClient.Builder] so all CloudStream + * HTTP traffic is routed through [server]. + */ +fun OkHttpClient.Builder.addVpnProxy(server: VpnServer): OkHttpClient.Builder { + val proxy = Proxy(server.proxyType, InetSocketAddress.createUnresolved(server.host, server.proxyPort)) + proxy(proxy) + if (server.username != null && server.password != null) { + proxyAuthenticator { _, response -> + response.request.newBuilder() + .header("Proxy-Authorization", + okhttp3.Credentials.basic(server.username, server.password)) + .build() + } + } + return this +} + +// ───────────────────────────────────────────────────────────────────────────── +// Dynamic Proxy Discovery +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Returns a list of available proxy countries and their counts. + * The first item is always ("ANY", totalCount). + * The rest are specific countries sorted by count (descending). + */ +suspend fun getVpnCountryChoices(): List> { + val list = fetchProxiflyList() + val total = list.size + val grouped = list.groupingBy { it.third }.eachCount() + val choices = mutableListOf>() + choices.add("ANY" to total) + choices.addAll( + grouped.entries + .filter { it.key.isNotBlank() } + .map { it.key to it.value } + .sortedByDescending { it.second } + ) + return choices +} + +// ───────────────────────────────────────────────────────────────────────────── +// Live proxy cache (read by buildDefaultClient without async calls) +// ───────────────────────────────────────────────────────────────────────────── + +/** + * The currently active, tested SOCKS5 proxy. + * Null = no proxy applied. Set by [resolveAndTestVpnServer]. + * + * [@Volatile] ensures the write from the IO coroutine is immediately visible + * to any thread reading it (e.g. CS3IPlayer on the main thread deciding + * whether to bypass Cronet in favour of OkHttp). + */ +@Volatile +var currentVpnServer: VpnServer? = null + +/** + * Sets JVM-level system proxy properties so ALL Java socket connections + * (HttpURLConnection, any plain Socket, etc.) route through the SOCKS5 proxy, + * not just OkHttp clients that have an explicit proxy configured. + */ +fun setGlobalJvmProxy(host: String, port: Int) { + System.setProperty("socksProxyHost", host) + System.setProperty("socksProxyPort", port.toString()) + Log.d(TAG, "Global JVM SOCKS5 proxy set: $host:$port") +} + +/** Removes the JVM-level SOCKS5 proxy properties. */ +fun clearGlobalJvmProxy() { + System.clearProperty("socksProxyHost") + System.clearProperty("socksProxyPort") + Log.d(TAG, "Global JVM SOCKS5 proxy cleared") +} + +/** + * When true, the proxy probe ignores SSL certificate errors (expired/self-signed). + * This allows MITM-style proxies to pass the test but means HTTPS traffic + * can be intercepted by the proxy. Controlled by the user via Settings. + */ +@Volatile +var vpnAllowInsecure: Boolean = false + +// ───────────────────────────────────────────────────────────────────────────── +// End-to-end proxy test using OkHttp +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Tests a SOCKS5 proxy end-to-end: + * 1. Builds a temporary OkHttpClient with the proxy configured. + * 2. GETs http://connectivitycheck.gstatic.com/generate_204 through it. + * 3. Returns true only if the response code is 2xx (typically 204). + * + * This tests actual traffic forwarding — no fake handshakes or false positives. + */ +suspend fun testProxyWithOkHttp( + proxyType: Proxy.Type, + host: String, + port: Int, + timeoutSec: Long = 10L, + allowInsecure: Boolean = false, +): Boolean = withContext(Dispatchers.IO) { + try { + val proxy = Proxy(proxyType, InetSocketAddress.createUnresolved(host, port)) + val client = OkHttpClient.Builder() + .proxy(proxy) + .connectTimeout(timeoutSec, TimeUnit.SECONDS) + .readTimeout(timeoutSec, TimeUnit.SECONDS) + .writeTimeout(timeoutSec, TimeUnit.SECONDS) + .apply { if (allowInsecure) ignoreAllSSLErrors() } + .build() + val response = client.newCall( + Request.Builder() + // Use HTTPS so secure-mode rejects MITM proxies (invalid/expired cert). + // In allowInsecure mode SSL errors are ignored so MITM proxies pass too. + .url("https://www.google.com/generate_204") + .build() + ).execute() + val ok = response.code in 200..299 + response.close() + Log.d(TAG, "OkHttp probe $host:$port [$proxyType] -> HTTP ${response.code} ok=$ok") + ok + } catch (e: Exception) { + Log.v(TAG, "OkHttp probe $host:$port [$proxyType] failed: ${e.message}") + false + } +} + +/** + * Resolves and tests the VPN server for [countryCode]. + * + * Fetches Proxifly SOCKS5 servers (filtered by country), + * tests each one with a real OkHttp request, and stores the first working server + * in [currentVpnServer]. + * + * @return The first working [VpnServer], or null if no proxy succeeded. + */ +suspend fun resolveAndTestVpnServer(countryCode: String): VpnServer? { + if (countryCode == "NONE" || countryCode.isBlank()) { + currentVpnServer = null + clearGlobalJvmProxy() + return null + } + val allowInsecure = vpnAllowInsecure + val filterCountry = if (countryCode == "ANY") null else countryCode + val candidates = fetchProxiflyServers(filterCountry) + + if (candidates.isEmpty()) { + Log.w(TAG, "No Proxifly candidates for country=$countryCode") + currentVpnServer = null + return null + } + + Log.d(TAG, "Testing ${candidates.size} Proxifly proxies (country=$countryCode allowInsecure=$allowInsecure)…") + for (server in candidates) { + if (!testProxyWithOkHttp(Proxy.Type.SOCKS, server.host, server.port, allowInsecure = allowInsecure)) continue + + val verified = server.copy(proxyType = Proxy.Type.SOCKS, proxyPort = server.port) + currentVpnServer = verified + setGlobalJvmProxy(server.host, server.port) + Log.d(TAG, "Proxy confirmed: ${server.host}:${server.port} | insecure=$allowInsecure") + return verified + } + + Log.w(TAG, "All ${candidates.size} Proxifly proxies failed for country=$countryCode") + currentVpnServer = null + clearGlobalJvmProxy() + return null +} + +/** + * Called once on app launch to auto-restore the VPN connection. + * If the user has a VPN enabled, it first tests the last-known working proxy. + * If that proxy is offline, it searches for a new one in the selected region. + */ +fun initializeVpn(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + val vpnPref = settingsManager.getString(context.getString(R.string.vpn_country_pref_key), "NONE") ?: "NONE" + if (vpnPref == "NONE" || vpnPref.isBlank()) return + + vpnAllowInsecure = settingsManager.getBoolean(context.getString(R.string.vpn_ssl_pref_key), false) + + GlobalScope.launch(Dispatchers.IO) { + val savedJson = settingsManager.getString("vpn_last_server_json", null) + var restoredServer: VpnServer? = null + if (savedJson != null) { + try { + restoredServer = mapper.readValue(savedJson) + } catch (e: Exception) { + Log.w(TAG, "Failed to parse saved VPN server", e) + } + } + + if (restoredServer != null) { + Log.d(TAG, "Testing saved VPN server on boot: ${restoredServer.host}:${restoredServer.port} insecure=$vpnAllowInsecure") + val isAlive = testProxyWithOkHttp(Proxy.Type.SOCKS, restoredServer.host, restoredServer.port, allowInsecure = vpnAllowInsecure) + if (isAlive) { + currentVpnServer = restoredServer + setGlobalJvmProxy(restoredServer.host, restoredServer.port) + Log.d(TAG, "Saved VPN server restored successfully!") + // Notify client to rebuild if needed + com.lagradost.cloudstream3.app.initClient(context) + return@launch + } + Log.w(TAG, "Saved VPN server is offline. Falling back to fresh search...") + } + + // Saved proxy didn't work (or didn't exist). Find a new one. + val newServer = resolveAndTestVpnServer(vpnPref) + if (newServer != null) { + try { + settingsManager.edit().putString("vpn_last_server_json", mapper.writeValueAsString(newServer)).apply() + } catch (e: Exception) { + Log.e(TAG, "Failed to save new VPN server JSON", e) + } + com.lagradost.cloudstream3.app.initClient(context) + } else { + Log.e(TAG, "Failed to auto-connect to any VPN proxy on boot.") + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index d316f28cd19..7d0def1491d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -13,6 +13,7 @@ import android.util.Log import android.util.Rational import android.widget.FrameLayout import androidx.annotation.AnyThread +import com.lagradost.nicehttp.ignoreAllSSLErrors import androidx.annotation.MainThread import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog @@ -754,6 +755,15 @@ class CS3IPlayer : IPlayer { } fun tryCreateEngine(context: Context, diskCacheSize: Long): CronetEngine? { + // Cronet has its own native network stack — it ignores OkHttp proxy settings + // AND JVM system proxy properties (socksProxyHost etc.). When a VPN proxy is + // active we must disable Cronet entirely so all ExoPlayer requests go through + // OkHttpDataSource, which correctly uses the configured SOCKS5 proxy. + if (com.lagradost.cloudstream3.network.currentVpnServer != null) { + Log.d(TAG, "VPN active — skipping CronetEngine, using OkHttp for all streams") + return null + } + // Fast case, no need to recreate it cronetEngine?.let { return it @@ -797,12 +807,26 @@ class CS3IPlayer : IPlayer { }?.value ?: USER_AGENT val source = if (interceptor == null) { - if (engine == null) { - Log.d(TAG, "Using DefaultHttpDataSource for $link") - OkHttpDataSource.Factory(app.baseClient).setUserAgent(userAgent) + // Cronet has its own network stack and ignores OkHttp's proxy settings. + // When a VPN proxy is active we must use OkHttpDataSource so traffic + // is routed through the configured SOCKS5 proxy. + val vpnServer = com.lagradost.cloudstream3.network.currentVpnServer + val canUseCronet = engine != null && vpnServer == null + Log.d(TAG, "VPN_DEBUG createVideoSource: vpnServer=$vpnServer engine=${engine != null} canUseCronet=$canUseCronet") + Log.d(TAG, "VPN_DEBUG baseClient proxy=${app.baseClient.proxy}") + if (!canUseCronet) { + // When VPN insecure mode is enabled, build a client that also skips SSL + // validation so MITM proxies can tunnel HTTPS streams without errors. + val client = if (vpnServer != null && com.lagradost.cloudstream3.network.vpnAllowInsecure) { + app.baseClient.newBuilder().ignoreAllSSLErrors().build() + } else { + app.baseClient + } + Log.d(TAG, "Using OkHttpDataSource for $link (vpn=${vpnServer?.host})") + OkHttpDataSource.Factory(client).setUserAgent(userAgent) } else { Log.d(TAG, "Using CronetDataSource for $link") - CronetDataSource.Factory(engine, Executors.newSingleThreadExecutor()) + CronetDataSource.Factory(engine!!, Executors.newSingleThreadExecutor()) .setUserAgent(userAgent) .setConnectionTimeoutMs(CRONET_TIMEOUT_MS) .setReadTimeoutMs(CRONET_TIMEOUT_MS) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 354424978b9..5ae1d37b5d2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.settings import android.content.Context import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -25,7 +26,10 @@ import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding import com.lagradost.cloudstream3.databinding.AddSiteInputBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe +import com.lagradost.cloudstream3.network.currentVpnServer import com.lagradost.cloudstream3.network.initClient +import com.lagradost.cloudstream3.network.resolveAndTestVpnServer +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -340,6 +344,117 @@ class SettingsGeneral : BasePreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + // ── VPN SSL mode toggle ─────────────────────────────────────────────── + val vpnSslPref = getPref(R.string.vpn_ssl_key) as? androidx.preference.SwitchPreference + vpnSslPref?.let { pref -> + // Restore persisted value into the in-memory flag on fragment start + val savedInsecure = settingsManager.getBoolean(getString(R.string.vpn_ssl_pref_key), false) + com.lagradost.cloudstream3.network.vpnAllowInsecure = savedInsecure + pref.isChecked = savedInsecure + pref.summary = if (savedInsecure) + getString(R.string.vpn_ssl_summary_on) + else + getString(R.string.vpn_ssl_summary_off) + + pref.setOnPreferenceChangeListener { _, newValue -> + val insecure = newValue as Boolean + com.lagradost.cloudstream3.network.vpnAllowInsecure = insecure + settingsManager.edit { putBoolean(getString(R.string.vpn_ssl_pref_key), insecure) } + pref.summary = if (insecure) + getString(R.string.vpn_ssl_summary_on) + else + getString(R.string.vpn_ssl_summary_off) + // If a VPN is already active, rebuild the client so the SSL policy + // takes effect without requiring the user to re-select the VPN. + val appCtx = context ?: CloudStreamApp.context + if (com.lagradost.cloudstream3.network.currentVpnServer != null && appCtx != null) { + app.initClient(appCtx) + } + true + } + } + + getPref(R.string.vpn_key)?.setOnPreferenceClickListener { + val currentVpn = settingsManager.getString(getString(R.string.vpn_country_pref_key), "NONE") ?: "NONE" + showToast("Fetching available VPN regions...", android.widget.Toast.LENGTH_SHORT) + + this.ioSafe { + val choices = com.lagradost.cloudstream3.network.getVpnCountryChoices() + val names = mutableListOf("None") + val values = mutableListOf("NONE") + for (choice in choices) { + val code = choice.first + val count = choice.second + val name = if (code == "ANY") "Auto (\u200B$count)" else "$code (\u200B$count)" + names.add(name) + values.add(code) + } + + activity?.runOnUiThread { + activity?.showBottomDialog( + names, + values.indexOf(currentVpn).takeIf { it >= 0 } ?: 0, + getString(R.string.vpn_pref), + true, + {} + ) { selectedIndex -> + val selectedValue = values[selectedIndex] + val appCtx = context ?: CloudStreamApp.context + + if (selectedValue == "NONE") { + // User selected "None" – clear proxy and re-init + settingsManager.edit { putString(getString(R.string.vpn_country_pref_key), "NONE") } + com.lagradost.cloudstream3.network.currentVpnServer = null + appCtx?.let { app.initClient(it) } + } else { + // Save preference optimistically, then test the proxy + settingsManager.edit { putString(getString(R.string.vpn_country_pref_key), selectedValue) } + showToast(getString(R.string.vpn_connecting), android.widget.Toast.LENGTH_SHORT) + + if (appCtx != null) { + this@SettingsGeneral.ioSafe { + try { + val server = com.lagradost.cloudstream3.network.resolveAndTestVpnServer(selectedValue) + + if (server != null) { + // Proxy confirmed SOCKS5 – rebuild client with proxy + try { + settingsManager.edit { putString("vpn_last_server_json", com.lagradost.cloudstream3.mapper.writeValueAsString(server)) } + } catch (e: Exception) { + Log.e("VpnProviders", "Failed to save VPN server JSON", e) + } + app.initClient(appCtx) + showToast( + getString(R.string.vpn_connected).format(server.name), + android.widget.Toast.LENGTH_LONG + ) + } else { + // Proxy test failed: revert the preference so the next + // buildDefaultClient won't try a broken proxy + settingsManager.edit { putString(getString(R.string.vpn_country_pref_key), "NONE") } + com.lagradost.cloudstream3.network.currentVpnServer = null + app.initClient(appCtx) + showToast( + getString(R.string.vpn_proxy_unavailable), + android.widget.Toast.LENGTH_LONG + ) + } + } catch (e: Exception) { + logError(e) + settingsManager.edit { putString(getString(R.string.vpn_country_pref_key), "NONE") } + com.lagradost.cloudstream3.network.currentVpnServer = null + app.initClient(appCtx) + showToast(R.string.vpn_error, android.widget.Toast.LENGTH_SHORT) + } + } + } + } + } + } + } + return@setOnPreferenceClickListener true + } + fun getDownloadDirs(): List { return safe { context?.let { ctx -> diff --git a/app/src/main/res/drawable/ic_baseline_vpn_key_24.xml b/app/src/main/res/drawable/ic_baseline_vpn_key_24.xml new file mode 100644 index 00000000000..fd189750a97 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_vpn_key_24.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/values/donottranslate-strings.xml b/app/src/main/res/values/donottranslate-strings.xml index 6a4c8271341..77f699ac6da 100644 --- a/app/src/main/res/values/donottranslate-strings.xml +++ b/app/src/main/res/values/donottranslate-strings.xml @@ -48,6 +48,11 @@ provider_lang_key dns_key jsdelivr_proxy_key + vpn_key + vpn_pref_key + vpn_country_pref_key + vpn_ssl_key + vpn_ssl_pref_key download_path_key download_parallel_key download_concurrent_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cf951cf5f..774e94189fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -343,6 +343,16 @@ Causes problems if set too high on devices with low storage space, such as Android TV. DNS over HTTPS Useful for bypassing ISP blocks + VPN Proxy + Route traffic through a free VPN proxy to bypass regional blocks + Connecting to VPN… + Connected to %s + Failed to connect to VPN server + VPN proxy unavailable: no working proxy found (tried SOCKS5 + HTTP CONNECT on ports 1080, 8080, 3128…). Try a different server or check your network. + Fetching VPN servers… + VPN – Allow Insecure Proxies + SSL errors ignored — more proxies available, but traffic may be intercepted + Secure — only proxies with valid SSL certificates are used GitHub Proxy Could not reach GitHub. Turning on jsDelivr proxy… Bypass blocking of raw github URLs using jsDelivr. May cause updates to be delayed by few days. diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index f8b9b4f452f..ed35e01b19b 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -69,6 +69,20 @@ android:summary="@string/dns_pref_summary" android:title="@string/dns_pref" /> + + + +