Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
373 changes: 373 additions & 0 deletions app/src/main/java/com/lagradost/cloudstream3/network/VpnProviders.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String>("None")
val values = mutableListOf<String>("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<String> {
return safe {
context?.let { ctx ->
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/res/drawable/ic_baseline_vpn_key_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/white">
<path
android:fillColor="@android:color/white"
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/values/donottranslate-strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
<string name="provider_lang_key">provider_lang_key</string>
<string name="dns_key">dns_key</string>
<string name="jsdelivr_proxy_key">jsdelivr_proxy_key</string>
<string name="vpn_key">vpn_key</string>
<string name="vpn_pref_key">vpn_pref_key</string>
<string name="vpn_country_pref_key">vpn_country_pref_key</string>
<string name="vpn_ssl_key">vpn_ssl_key</string>
<string name="vpn_ssl_pref_key">vpn_ssl_pref_key</string>
<string name="download_path_key">download_path_key</string>
<string name="download_parallel_key">download_parallel_key</string>
<string name="download_concurrent_key">download_concurrent_key</string>
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@
<string name="video_disk_description">Causes problems if set too high on devices with low storage space, such as Android TV.</string>
<string name="dns_pref">DNS over HTTPS</string>
<string name="dns_pref_summary">Useful for bypassing ISP blocks</string>
<string name="vpn_pref">VPN Proxy</string>
<string name="vpn_pref_summary">Route traffic through a free VPN proxy to bypass regional blocks</string>
<string name="vpn_connecting">Connecting to VPN…</string>
<string name="vpn_connected">Connected to %s</string>
<string name="vpn_error">Failed to connect to VPN server</string>
<string name="vpn_proxy_unavailable">VPN proxy unavailable: no working proxy found (tried SOCKS5 + HTTP CONNECT on ports 1080, 8080, 3128…). Try a different server or check your network.</string>
<string name="vpn_fetching_servers">Fetching VPN servers…</string>
<string name="vpn_ssl_title">VPN – Allow Insecure Proxies</string>
<string name="vpn_ssl_summary_on">SSL errors ignored — more proxies available, but traffic may be intercepted</string>
<string name="vpn_ssl_summary_off">Secure — only proxies with valid SSL certificates are used</string>
<string name="jsdelivr_proxy">GitHub Proxy</string>
<string name="jsdelivr_enabled">Could not reach GitHub. Turning on jsDelivr proxy…</string>
<string name="jsdelivr_proxy_summary">Bypass blocking of raw github URLs using jsDelivr. May cause updates to be delayed by few days.</string>
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/res/xml/settings_general.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@
android:summary="@string/dns_pref_summary"
android:title="@string/dns_pref" />

<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_baseline_vpn_key_24"
android:key="@string/vpn_ssl_key"
android:summaryOff="@string/vpn_ssl_summary_off"
android:summaryOn="@string/vpn_ssl_summary_on"
android:title="@string/vpn_ssl_title" />

<Preference
android:icon="@drawable/ic_baseline_vpn_key_24"
android:key="@string/vpn_key"
android:summary="@string/vpn_pref_summary"
android:title="@string/vpn_pref" />

<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_github_logo"
Expand Down