|
| 1 | +package com.csguard |
| 2 | + |
| 3 | +/** |
| 4 | + * Known ad-network / popunder domains used by malicious CloudStream extensions. |
| 5 | + * |
| 6 | + * This list is seeded from a real-world analysis of the |
| 7 | + * `NivinCNC/CNCVerse-Cloud-Stream-Extension` repo, where the constant |
| 8 | + * |
| 9 | + * OMG10 = "aHR0cHM6Ly9vbWcxMC5jb20vNC8xMTEwNDQ4OQ==" |
| 10 | + * |
| 11 | + * base64-decodes to `https://omg10.com/4/11104489` — an OMG/PropellerAds-style |
| 12 | + * popunder ad-network URL fired via raw `Intent.ACTION_VIEW` from inside |
| 13 | + * `MainAPI.loadLinks()` on every Play tap. |
| 14 | + * |
| 15 | + * The list is intentionally broad: extensions like this rotate ad networks, so |
| 16 | + * we block by category rather than just by the single observed domain. |
| 17 | + */ |
| 18 | +object AdBlockList { |
| 19 | + |
| 20 | + // ---- Hard-blocked hosts (known ad networks / popunders / redirectors) ---- |
| 21 | + val BLOCKED_HOSTS: Set<String> = setOf( |
| 22 | + // Observed in CNCVerse repo |
| 23 | + "omg10.com", |
| 24 | + "omg1.com", "omg2.com", "omg3.com", "omg4.com", "omg5.com", |
| 25 | + "omg6.com", "omg7.com", "omg8.com", "omg9.com", |
| 26 | + |
| 27 | + // PropellerAds / Monetag family |
| 28 | + "propellerads.com", "propeller-tracking.com", |
| 29 | + "monetag.com", "monetag-handhub.com", |
| 30 | + |
| 31 | + // Adsterra |
| 32 | + "adsterra.com", "adsterra.network", "prohitshere.com", |
| 33 | + |
| 34 | + // HilltopAds |
| 35 | + "hilltopads.com", "hilltopads.net", "hilltopads-delivery.com", |
| 36 | + |
| 37 | + // PopAds / PopCash |
| 38 | + "popads.net", "popcash.net", "popmyads.com", |
| 39 | + |
| 40 | + // Ad-Maven / Maven |
| 41 | + "ad-maven.com", "a-mo.net", "mvn.link", |
| 42 | + |
| 43 | + // Shortcutters commonly used by ad-redirect layers |
| 44 | + "bit.ly", "t.ly", "cutt.ly", "shorturl.at", "tinyurl.com", |
| 45 | + "shrtco.de", "soo.gd", "s.id", "is.gd", "v.gd", |
| 46 | + "linkvertise.com", "linkvertise.net", |
| 47 | + |
| 48 | + // Generic popunder / malvertising CDNs |
| 49 | + "onclickperformance.com", "onclickprediction.com", |
| 50 | + "onclickscript.com", "highperformanceformat.com", |
| 51 | + "pushmonetization.com", "realtimepush.com", |
| 52 | + "highratecpm.com", "profithighrate.com", |
| 53 | + "crazypushsub.com", "push-notification.tools", |
| 54 | + "bemobtrcks.com", "bemobpath.com", |
| 55 | + "go2cloud.org", "go2affise.com", |
| 56 | + "trackings.selfpublishing.com", |
| 57 | + "xl-trail.com", "trail_trk.com", |
| 58 | + |
| 59 | + // Suspicious TLD patterns commonly abused |
| 60 | + // (will be matched more carefully in code — see isBlocked()) |
| 61 | + ) |
| 62 | + |
| 63 | + // ---- Hosts that are explicitly ALLOWED to open externally ---- |
| 64 | + // |
| 65 | + // CloudStream itself opens these for repo install / legal / community |
| 66 | + // purposes, and they are user-initiated actions — not silent ad redirects. |
| 67 | + val SAFE_HOSTS: Set<String> = setOf( |
| 68 | + // CloudStream infra |
| 69 | + "cs.repo", "cloudstream.on.fleek.co", |
| 70 | + "recloudstream.github.io", "github.com", "raw.githubusercontent.com", |
| 71 | + |
| 72 | + // Self-promo (user-initiated, in the form of a dialog with a button) |
| 73 | + "t.me", "telegram.me", |
| 74 | + |
| 75 | + // Common legitimate support sites |
| 76 | + "discord.com", "discord.gg", "patreon.com", "ko-fi.com", |
| 77 | + "buymeacoffee.com", |
| 78 | + |
| 79 | + // Search/lookup that user might legitimately want |
| 80 | + "www.google.com", "duckduckgo.com", |
| 81 | + |
| 82 | + // Wikipedia / IMDB for metadata |
| 83 | + "wikipedia.org", "imdb.com" |
| 84 | + ) |
| 85 | + |
| 86 | + // ---- URL patterns that are NEVER media stream URLs ---- |
| 87 | + // |
| 88 | + // If an ExtractorLink or PlayInBrowser target matches one of these |
| 89 | + // patterns, it's almost certainly an ad/redirect, not a real video. |
| 90 | + val NON_MEDIA_PATH_PATTERNS: List<Regex> = listOf( |
| 91 | + Regex("/\\d+/(\\d{6,})"), // /4/11104489 style ad zone IDs |
| 92 | + Regex("/(popunder|popunderinit)"), |
| 93 | + Regex("/(redirect|go|visit|jump)/[a-zA-Z0-9]+"), |
| 94 | + Regex("/watch\\?key="), |
| 95 | + Regex("click\\?"), |
| 96 | + Regex("/aff(_)?id=") |
| 97 | + ) |
| 98 | + |
| 99 | + /** |
| 100 | + * Returns true if the host (or any of its parent domains) is on the |
| 101 | + * hard-block list. |
| 102 | + */ |
| 103 | + fun isHostBlocked(host: String?): Boolean { |
| 104 | + if (host.isNullOrBlank()) return false |
| 105 | + val h = host.lowercase().trim() |
| 106 | + // exact match |
| 107 | + if (h in BLOCKED_HOSTS) return true |
| 108 | + // suffix match (handles subdomains like ads.omg10.com) |
| 109 | + for (blocked in BLOCKED_HOSTS) { |
| 110 | + if (h == blocked || h.endsWith(".$blocked")) return true |
| 111 | + } |
| 112 | + return false |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * Returns true if the host is explicitly allowed to open externally. |
| 117 | + * |
| 118 | + * Combines three sources: |
| 119 | + * 1. [SAFE_HOSTS] — static baseline of CloudStream-essential hosts |
| 120 | + * 2. [AllowlistStore.alwaysAllow] — persistent user-approved hosts |
| 121 | + * 3. [AllowlistStore] session allow-once — process-scoped one-shot approvals |
| 122 | + * |
| 123 | + * Always called from the [GuardPolicy.shouldBlock] hot path, so it must be fast. |
| 124 | + */ |
| 125 | + fun isHostSafe(host: String?): Boolean { |
| 126 | + if (host.isNullOrBlank()) return false |
| 127 | + val h = host.lowercase().trim() |
| 128 | + // 1. Static baseline |
| 129 | + if (h in SAFE_HOSTS) return true |
| 130 | + for (safe in SAFE_HOSTS) { |
| 131 | + if (h == safe || h.endsWith(".$safe")) return true |
| 132 | + } |
| 133 | + // 2 & 3. User-managed allowlist (persistent + session) |
| 134 | + return try { |
| 135 | + AllowlistStore.isAllowed(h) |
| 136 | + } catch (_: Throwable) { |
| 137 | + // AllowlistStore not initialized yet — be safe, fall back to baseline |
| 138 | + false |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + /** |
| 143 | + * Heuristic: does the URL path look like an ad-redirect path |
| 144 | + * rather than a real media file? |
| 145 | + */ |
| 146 | + fun looksLikeAdPath(url: String?): Boolean { |
| 147 | + if (url.isNullOrBlank()) return false |
| 148 | + return NON_MEDIA_PATH_PATTERNS.any { it.containsMatchIn(url) } |
| 149 | + } |
| 150 | +} |
0 commit comments