Skip to content

Commit 0bfd9e9

Browse files
errorcodeQQerrorcodeQQ
authored andcommitted
feat: add CSGuard
1 parent 3713b1c commit 0bfd9e9

20 files changed

Lines changed: 1314 additions & 48 deletions

AniZen/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ cloudstream {
1818
iconUrl = "https://anizen.tr/favicon.png"
1919
isCrossPlatform = true
2020
}
21+

CSGuard/build.gradle.kts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
version = 1
2+
3+
cloudstream {
4+
description = "Defensive CloudStream plugin that intercepts and neutralizes malicious browser-redirect ad injections from other extensions. Wraps provider Context objects so raw Intent.ACTION_VIEW calls to ad networks are silently dropped to the void."
5+
authors = listOf("CSGuard")
6+
status = 1
7+
tvTypes = listOf("Others")
8+
requiresResources = false
9+
language = "en"
10+
iconUrl = "https://raw.githubusercontent.com/google/material-design-icons/master/png/communication/security/png48/security_48dp.png"
11+
}
12+
13+
android {
14+
namespace = "com.csguard"
15+
lint { abortOnError = false }
16+
buildFeatures {
17+
buildConfig = true
18+
viewBinding = false
19+
}
20+
}

CSGuard/gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2+
android.useAndroidX=true
3+
android.enableJetifier=true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest />
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.csguard
2+
3+
object AdBlockList {
4+
5+
val BLOCKED_HOSTS: Set<String> = setOf(
6+
7+
"omg10.com",
8+
"omg1.com", "omg2.com", "omg3.com", "omg4.com", "omg5.com",
9+
"omg6.com", "omg7.com", "omg8.com", "omg9.com",
10+
11+
"propellerads.com", "propeller-tracking.com",
12+
"monetag.com", "monetag-handhub.com",
13+
14+
"adsterra.com", "adsterra.network", "prohitshere.com",
15+
16+
"hilltopads.com", "hilltopads.net", "hilltopads-delivery.com",
17+
18+
"popads.net", "popcash.net", "popmyads.com",
19+
20+
"ad-maven.com", "a-mo.net", "mvn.link",
21+
22+
"bit.ly", "t.ly", "cutt.ly", "shorturl.at", "tinyurl.com",
23+
"shrtco.de", "soo.gd", "s.id", "is.gd", "v.gd",
24+
"linkvertise.com", "linkvertise.net",
25+
26+
"onclickperformance.com", "onclickprediction.com",
27+
"onclickscript.com", "highperformanceformat.com",
28+
"pushmonetization.com", "realtimepush.com",
29+
"highratecpm.com", "profithighrate.com",
30+
"crazypushsub.com", "push-notification.tools",
31+
"bemobtrcks.com", "bemobpath.com",
32+
"go2cloud.org", "go2affise.com",
33+
"trackings.selfpublishing.com",
34+
"xl-trail.com", "trail_trk.com",
35+
36+
)
37+
38+
val SAFE_HOSTS: Set<String> = setOf(
39+
40+
"cs.repo", "cloudstream.on.fleek.co",
41+
"recloudstream.github.io", "github.com", "raw.githubusercontent.com",
42+
43+
"t.me", "telegram.me",
44+
45+
"discord.com", "discord.gg", "patreon.com", "ko-fi.com",
46+
"buymeacoffee.com",
47+
48+
"www.google.com", "duckduckgo.com",
49+
50+
"wikipedia.org", "imdb.com"
51+
)
52+
53+
val NON_MEDIA_PATH_PATTERNS: List<Regex> = listOf(
54+
Regex("/\\d+/(\\d{6,})"), // /4/11104489 style ad zone IDs
55+
Regex("/(popunder|popunderinit)"),
56+
Regex("/(redirect|go|visit|jump)/[a-zA-Z0-9]+"),
57+
Regex("/watch\\?key="),
58+
Regex("click\\?"),
59+
Regex("/aff(_)?id=")
60+
)
61+
62+
fun isHostBlocked(host: String?): Boolean {
63+
if (host.isNullOrBlank()) return false
64+
val h = host.lowercase().trim()
65+
66+
if (h in BLOCKED_HOSTS) return true
67+
68+
for (blocked in BLOCKED_HOSTS) {
69+
if (h == blocked || h.endsWith(".$blocked")) return true
70+
}
71+
return false
72+
}
73+
74+
fun isHostSafe(host: String?): Boolean {
75+
if (host.isNullOrBlank()) return false
76+
val h = host.lowercase().trim()
77+
78+
if (h in SAFE_HOSTS) return true
79+
for (safe in SAFE_HOSTS) {
80+
if (h == safe || h.endsWith(".$safe")) return true
81+
}
82+
83+
return try {
84+
AllowlistStore.isAllowed(h)
85+
} catch (_: Throwable) {
86+
87+
false
88+
}
89+
}
90+
91+
fun looksLikeAdPath(url: String?): Boolean {
92+
if (url.isNullOrBlank()) return false
93+
return NON_MEDIA_PATH_PATTERNS.any { it.containsMatchIn(url) }
94+
}
95+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.csguard
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import android.util.Log
6+
import org.json.JSONArray
7+
import org.json.JSONObject
8+
9+
object AllowlistStore {
10+
11+
private const val TAG = "CSGuard"
12+
private const val PREFS_NAME = "csguard_allowlist"
13+
private const val KEY_ALWAYS = "always_allow_hosts"
14+
private const val KEY_BLOCKED_PROVIDERS = "blocked_providers"
15+
private const val KEY_BLOCKED = "blocked_attempts_log"
16+
private const val MAX_BLOCKED_LOG = 200
17+
18+
@Volatile private var prefs: SharedPreferences? = null
19+
20+
private val sessionAllowOnce = java.util.Collections.synchronizedSet(HashSet<String>())
21+
22+
fun init(context: Context) {
23+
if (prefs != null) return
24+
prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
25+
Log.i(TAG, "AllowlistStore initialized — alwaysAllow=${alwaysAllow().size} hosts")
26+
}
27+
28+
fun alwaysAllow(): Set<String> {
29+
val raw = prefs?.getString(KEY_ALWAYS, "[]") ?: "[]"
30+
return try {
31+
val arr = JSONArray(raw)
32+
(0 until arr.length()).map { arr.getString(it).lowercase().trim() }.toSet()
33+
} catch (_: Throwable) { emptySet() }
34+
}
35+
36+
fun addAlwaysAllow(host: String): Boolean {
37+
val normalized = host.lowercase().trim()
38+
if (normalized.isEmpty()) return false
39+
val current = alwaysAllow().toMutableSet()
40+
if (!current.add(normalized)) return false
41+
prefs?.edit()?.putString(KEY_ALWAYS, JSONArray(current.toList()).toString())?.apply()
42+
Log.i(TAG, "AllowlistStore: added always-allow → $normalized")
43+
return true
44+
}
45+
46+
fun removeAlwaysAllow(host: String): Boolean {
47+
val normalized = host.lowercase().trim()
48+
val current = alwaysAllow().toMutableSet()
49+
if (!current.remove(normalized)) return false
50+
prefs?.edit()?.putString(KEY_ALWAYS, JSONArray(current.toList()).toString())?.apply()
51+
Log.i(TAG, "AllowlistStore: removed always-allow → $normalized")
52+
return true
53+
}
54+
55+
fun blockedProviders(): Set<String> {
56+
val raw = prefs?.getString(KEY_BLOCKED_PROVIDERS, "[]") ?: "[]"
57+
return try {
58+
val arr = JSONArray(raw)
59+
(0 until arr.length()).map { arr.getString(it).trim() }.toSet()
60+
} catch (_: Throwable) { emptySet() }
61+
}
62+
63+
fun addBlockedProvider(providerName: String): Boolean {
64+
val normalized = providerName.trim()
65+
if (normalized.isEmpty()) return false
66+
val current = blockedProviders().toMutableSet()
67+
if (!current.add(normalized)) return false
68+
prefs?.edit()?.putString(KEY_BLOCKED_PROVIDERS, JSONArray(current.toList()).toString())?.apply()
69+
Log.i(TAG, "AllowlistStore: added blocked provider → $normalized")
70+
return true
71+
}
72+
73+
fun removeBlockedProvider(providerName: String): Boolean {
74+
val normalized = providerName.trim()
75+
val current = blockedProviders().toMutableSet()
76+
if (!current.remove(normalized)) return false
77+
prefs?.edit()?.putString(KEY_BLOCKED_PROVIDERS, JSONArray(current.toList()).toString())?.apply()
78+
Log.i(TAG, "AllowlistStore: removed blocked provider → $normalized")
79+
return true
80+
}
81+
82+
fun allowOnce(host: String) {
83+
sessionAllowOnce.add(host.lowercase().trim())
84+
Log.i(TAG, "AllowlistStore: allow-once (session) → $host")
85+
}
86+
87+
fun clearSessionAllowOnce() {
88+
sessionAllowOnce.clear()
89+
}
90+
91+
fun isAllowed(host: String?): Boolean {
92+
if (host.isNullOrBlank()) return false
93+
val h = host.lowercase().trim()
94+
if (h in sessionAllowOnce) return true
95+
val always = alwaysAllow()
96+
if (h in always) return true
97+
98+
for (allowed in always) {
99+
if (h.endsWith(".$allowed")) return true
100+
}
101+
return false
102+
}
103+
104+
data class BlockedEntry(
105+
val url: String,
106+
val caller: String,
107+
val timestamp: Long
108+
)
109+
110+
fun blockedLog(): List<BlockedEntry> {
111+
val raw = prefs?.getString(KEY_BLOCKED, "[]") ?: "[]"
112+
return try {
113+
val arr = JSONArray(raw)
114+
(0 until arr.length()).map { idx ->
115+
val obj = arr.getJSONObject(idx)
116+
BlockedEntry(
117+
url = obj.optString("url"),
118+
caller = obj.optString("caller"),
119+
timestamp = obj.optLong("ts")
120+
)
121+
}
122+
} catch (_: Throwable) { emptyList() }
123+
}
124+
125+
fun recordBlocked(url: String, caller: String) {
126+
val current = blockedLog().toMutableList()
127+
current.add(0, BlockedEntry(url, caller, System.currentTimeMillis()))
128+
129+
val trimmed = current.take(MAX_BLOCKED_LOG)
130+
val arr = JSONArray()
131+
for (entry in trimmed) {
132+
val obj = JSONObject()
133+
obj.put("url", entry.url)
134+
obj.put("caller", entry.caller)
135+
obj.put("ts", entry.timestamp)
136+
arr.put(obj)
137+
}
138+
prefs?.edit()?.putString(KEY_BLOCKED, arr.toString())?.apply()
139+
}
140+
141+
fun clearBlockedLog() {
142+
prefs?.edit()?.remove(KEY_BLOCKED)?.apply()
143+
}
144+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.csguard
2+
3+
object BlockedLog {
4+
@JvmStatic
5+
fun record(url: String, caller: String) {
6+
try {
7+
AllowlistStore.recordBlocked(url, caller)
8+
} catch (_: Throwable) {
9+
10+
}
11+
}
12+
13+
@JvmStatic
14+
fun all(): List<AllowlistStore.BlockedEntry> =
15+
try { AllowlistStore.blockedLog() } catch (_: Throwable) { emptyList() }
16+
17+
@JvmStatic
18+
fun clear() {
19+
try { AllowlistStore.clearBlockedLog() } catch (_: Throwable) {}
20+
}
21+
}

0 commit comments

Comments
 (0)