working plexamp and started readme

This commit is contained in:
Mathieu Broillet 2025-08-24 21:50:39 +02:00
parent a4f6030631
commit 45f0756191
Signed by: mathieub
GPG Key ID: 4428608CDA3A98D3
3 changed files with 296 additions and 5 deletions

127
README.md
View File

@ -1,2 +1,127 @@
# Plex Premium Hack # Plex Premium Hack
WIP
This repository contains a "mock" proxy that sits in your network and tricks Plex into thinking you have a Plex Premium
subscription.
### Requirements
- A router that can redirect traffic (i.e. OPNsense, pfSense, DD-WRT...)
- (alternative) a DNS server that can redirect traffic *(some apps won't work due to DNS pinning)*
- A reverse proxy (i.e. Traefik, Nginx, Caddy...)
## How to setup ?
Due to the nature of this hack, you'll have to :
- generate a new certificate authority (CA) for the proxy
- trust or patch the CA on clients and/or apps that will connect to your Plex server
### 1. Generate a new Certificate Authority (CA)
in writing...
### 2. Setup reverse proxy
In my case I'm using Traefik, so here is an example configuration :
```yaml
tls:
certificates:
# use certificates generated in step 1
- certFile: /etc/traefik/ssl/custom/plexfakeclients.crt
keyFile: /etc/traefik/ssl/custom/plexfakeclients.key
http:
routers:
plex:
entryPoints:
- https
service: plex
rule: Host(`plex.<your-domain>.com`)
# you may want to use TLS here too (don't use the custom CA cert generated in step 1)
plex_proxy:
entryPoints:
- https
service: plex_proxy
rule: Host(`clients.plex.tv`) || Host(`plex.tv`)
tls: { }
services:
plex:
loadBalancer:
servers:
- url: http://<plex-machine-ip>:32400
plex_proxy:
loadBalancer:
servers:
- url: http://<machine-where-proxy-is>:8000
```
### 3. Redirect traffic
For this to work we need to redirect the domain `clients.plex.tv` and `plex.tv` to our proxy.
This is easily done if you own a router that can do this but might be tricky if you don't.
> [!IMPORTANT]
> Mobile/desktop apps tends to use hardcoded DNS servers so if you don't have a router that can redirect traffic, you
> will not be able to use this hack.
> It might be possible to patch the app to use a custom DNS server but the apps are usually obfuscated and it's not easy
> to do so.
#### OPNsense / pfSense
First, find the IP address behind the plex domains.
```bash
dig clients.plex.tv +short
# 172.64.151.205
# 104.18.36.51
dig plex.tv +short
# 52.17.59.150
# 52.49.56.127
```
Then go into `Firewall` > `Aliases` and create two aliases:
- `plex_ips`
- Type: Host(s)
- Content: <the 4 IPs you found above>
- `plex_do_not_proxy`
- Type: Host(s)
- Content: <your plex server IP> and <your proxy server IP>
Then go into `Firewall` > `NAT` > `Port Forward` and create a new rule:
- Interface: `LAN`
- Protocol: `TCP`
- Source / Invert: [☑️]
- Source: *(select alias)* `plex_do_not_proxy`
- Source Port Range: `any`
- Destination: *(select alias)* `plex_ips`
- Destination Port Range: `443`
- Redirect Target IP: `<your proxy server IP>`
- Redirect Target Port: `443`
Finally go to `Firewall` > `NAT` > `Outbound` and create a new rule *(select Hybrid mode if needed)*:
- Interface: `LAN`
- TCP/IP Version: `IPv4`
- Protocol: `any`
- Source address: `any`
- Destination address: <your proxy server IP>
- Destination port : `443`
- Translation / target: `Interface address`
##### Test the redirection
Now if you try to go to `https://clients.plex.tv/api/hack` you should see a JSON response along the lines of :
```json
{
"status": "OK, Plex Pass features proxy enabled"
}
```
If you see the Plex "Oops, 404" page then something is wrong with your redirection or proxy.
## Patch PlexAmp

108
const.py
View File

@ -194,6 +194,111 @@ FEATURES_DICT = [{"id": "guided-upgrade", "uuid": "c9d9b7ee-fdd9-474e-b143-5039c
FEATURES = [feature["id"] for feature in FEATURES_DICT] FEATURES = [feature["id"] for feature in FEATURES_DICT]
PLEXAMP_FEATURES = [
"guided-upgrade",
"increase-password-complexity",
"upgrade-3ds2",
"ad-countdown-timer",
"adaptive_bitrate",
"amazon-loop-debug",
"Android - Dolby Vision",
"Android - PiP",
"avod-ad-analysis",
"avod-new-media",
"blacklist_get_signin",
"camera_upload",
"CU Sunset",
"client-radio-stations",
"cloudsync",
"cloudflare-turnstile-required",
"common-sense-media-ratings-premium",
"comments_and_replies_push_notifications",
"friend_request_push_notifications",
"community_access_plex_tv",
"community_friends_group_notifications",
"companions_sonos",
"content_filter",
"custom-home-removal",
"grandfather-sync",
"disable-facebook-auth",
"disable_home_user_friendships",
"disable_sharing_friendships",
"downloads-gating",
"drm_support",
"dvr",
"dvr-block-unsupported-countries",
"le_isrg_root_x1",
"epg-recent-channels",
"federated-auth",
"global-continue-watching",
"hwtranscode",
"hardware_transcoding",
"home",
"HRK_enable_EUR",
"imagga-v2",
"ios14-privacy-banner",
"item_clusters",
"iterable-notification-tokens",
"keep-payment-method",
"kevin-bacon",
"lets_encrypt",
"lightning-dvr-pivot",
"livetv",
"allow_dvr",
"live-tv-support-incomplete-segments",
"tuner-sharing",
"lyrics",
"metadata_search",
"vod_cloudflare",
"music_videos",
"new_plex_pass_prices",
"news-provider-sunset-modal",
"nominatim",
"pass",
"photos-favorites",
"photos-metadata-edition",
"photosV6-edit",
"photosV6-tv-albums",
"pms_health",
"premium-dashboard",
"premium_music_metadata",
"rate-limit-client-token",
"reactions_push_notifications_settings",
"shared_server_notification",
"shared_source_notification",
"redirect-subscription-to-account-page",
"scrobbling-service-plex-tv",
"album-types",
"collections",
"music-analysis",
"radio",
"session_bandwidth_restrictions",
"session_kick",
"exclude restrictions",
"signin_notification",
"signin_with_apple",
"skip-data-licensing-consent",
"sleep-timer",
"spring_serve_ad_provider",
"sync",
"trailers",
"transcoder_cache",
"boost-voices",
"TREBLE-show-features",
"silence-removal",
"sweet-fades",
"visualizers",
"volume-leveling",
"two-factor-authentication",
"unsupportedtuners",
"plexpass_from_billing_context",
"vod-schema",
"watch-together-invite",
"watchlist-rss",
"web_server_dashboard",
"webhooks"
]
ENTITLEMENTS = ["deprecated_google_iap_activation", "all", "roku", "android", "xbox_one", "xbox_360", "windows", ENTITLEMENTS = ["deprecated_google_iap_activation", "all", "roku", "android", "xbox_one", "xbox_360", "windows",
"windows_phone", "ios"] "windows_phone", "ios"]
@ -202,3 +307,6 @@ CURRENT_DATE_MINUS_ONE_DAY_Z = (datetime.now(timezone.utc) - timedelta(days=1)).
NEXT_MONTH_DATE_Z = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ") NEXT_MONTH_DATE_Z = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
CURRENT_DATE_Z = (datetime.now(timezone.utc)).strftime("%Y-%m-%dT%H:%M:%SZ") CURRENT_DATE_Z = (datetime.now(timezone.utc)).strftime("%Y-%m-%dT%H:%M:%SZ")
CURRENT_DATE_PLUS_30MIN_Z = (datetime.now(timezone.utc) + timedelta(minutes=30)).strftime("%Y-%m-%dT%H:%M:%SZ") CURRENT_DATE_PLUS_30MIN_Z = (datetime.now(timezone.utc) + timedelta(minutes=30)).strftime("%Y-%m-%dT%H:%M:%SZ")
TIMESTAMP_CURRENT = int(datetime.now().timestamp())
TIMESTAMP_CURRENT_MINUS_30MIN = int((datetime.now() - timedelta(minutes=30)).timestamp())
TIMESTAMP_CURRENT_PLUS_30DAYS = int((datetime.now() + timedelta(days=30)).timestamp())

View File

@ -7,7 +7,8 @@ from starlette.responses import JSONResponse
from client import AsyncCustomHost, NameSolver from client import AsyncCustomHost, NameSolver
from const import OFFICIAL_API, FEATURES_DICT, ENTITLEMENTS, CURRENT_DATE_MINUS_ONE_DAY, FEATURES, \ from const import OFFICIAL_API, FEATURES_DICT, ENTITLEMENTS, CURRENT_DATE_MINUS_ONE_DAY, FEATURES, \
CURRENT_DATE_MINUS_ONE_DAY_Z, NEXT_MONTH_DATE_Z CURRENT_DATE_MINUS_ONE_DAY_Z, NEXT_MONTH_DATE_Z, PLEXAMP_FEATURES, TIMESTAMP_CURRENT_PLUS_30DAYS, \
TIMESTAMP_CURRENT_MINUS_30MIN
app = FastAPI() app = FastAPI()
@ -235,14 +236,65 @@ async def fake_user_signin(request: Request):
"plan": "monthly", "plan": "monthly",
"paymentNotificationId": "1234567", "paymentNotificationId": "1234567",
"canUpgrade": True, "canUpgrade": True,
"features": FEATURES_DICT "features": PLEXAMP_FEATURES if "plexamp" in request.headers.get(
"X-Plex-Product").lower() else FEATURES_DICT
}, },
"subscriptions": [ "subscriptions": [
{ {
"id": 1234567, "id": 1234567,
"mode": "monthly", "mode": "monthly",
"startsAt": 1755963427, "startsAt": TIMESTAMP_CURRENT_MINUS_30MIN,
"renewsAt": 1758585600, "renewsAt": TIMESTAMP_CURRENT_PLUS_30DAYS,
"endsAt": None,
"billing": {
"paymentMethodId": 1234567,
"internalPaymentMethod": {}
},
"canceled": False,
"gracePeriod": False,
"onHold": False,
"canReactivate": False,
"canUpgrade": False,
"canDowngrade": False,
"canConvert": False,
"type": "plexpass",
"braintreeId": "xyzxyz",
"state": "active"
}
]
}
return await return_edited_response(upstream_response, data_override)
@app.get("/api/v2/user.json")
async def fake_get_user_json(request: Request):
upstream_response = await call_official(request, request.url.path.lstrip("/"))
data_override = None
content_type = upstream_response.headers.get("content-type", "")
if content_type.startswith("application/json"):
data_override = {
"subscriptionDescription": "Monthly Plex Pass",
"roles": ["plexpass"],
"entitlements": ENTITLEMENTS,
"subscription": {
"active": True,
"subscribedAt": CURRENT_DATE_MINUS_ONE_DAY_Z,
"status": "Active",
"paymentService": "braintree",
"plan": "monthly",
"paymentNotificationId": "1234567",
"canUpgrade": True,
"features": PLEXAMP_FEATURES if "plexamp" in request.headers.get(
"X-Plex-Product").lower() else FEATURES_DICT
},
"subscriptions": [
{
"id": 1234567,
"mode": "monthly",
"startsAt": TIMESTAMP_CURRENT_MINUS_30MIN,
"renewsAt": TIMESTAMP_CURRENT_PLUS_30DAYS,
"endsAt": None, "endsAt": None,
"billing": { "billing": {
"paymentMethodId": 1234567, "paymentMethodId": 1234567,
@ -265,6 +317,12 @@ async def fake_user_signin(request: Request):
return await return_edited_response(upstream_response, data_override) return await return_edited_response(upstream_response, data_override)
@app.get("/api/hack")
async def hack_endpoint():
return {"status": "OK, Plex Pass features proxy enabled"}
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def catch_all(request: Request, path: str): async def catch_all(request: Request, path: str):
upstream_response = await call_official(request, path) upstream_response = await call_official(request, path)