working plexamp and started readme
This commit is contained in:
parent
a4f6030631
commit
45f0756191
127
README.md
127
README.md
@ -1,2 +1,127 @@
|
||||
# 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
108
const.py
@ -194,6 +194,111 @@ FEATURES_DICT = [{"id": "guided-upgrade", "uuid": "c9d9b7ee-fdd9-474e-b143-5039c
|
||||
|
||||
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",
|
||||
"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")
|
||||
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")
|
||||
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())
|
66
main_app.py
66
main_app.py
@ -7,7 +7,8 @@ from starlette.responses import JSONResponse
|
||||
|
||||
from client import AsyncCustomHost, NameSolver
|
||||
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()
|
||||
|
||||
@ -235,14 +236,65 @@ async def fake_user_signin(request: Request):
|
||||
"plan": "monthly",
|
||||
"paymentNotificationId": "1234567",
|
||||
"canUpgrade": True,
|
||||
"features": FEATURES_DICT
|
||||
"features": PLEXAMP_FEATURES if "plexamp" in request.headers.get(
|
||||
"X-Plex-Product").lower() else FEATURES_DICT
|
||||
},
|
||||
"subscriptions": [
|
||||
{
|
||||
"id": 1234567,
|
||||
"mode": "monthly",
|
||||
"startsAt": 1755963427,
|
||||
"renewsAt": 1758585600,
|
||||
"startsAt": TIMESTAMP_CURRENT_MINUS_30MIN,
|
||||
"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,
|
||||
"billing": {
|
||||
"paymentMethodId": 1234567,
|
||||
@ -265,6 +317,12 @@ async def fake_user_signin(request: Request):
|
||||
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"])
|
||||
async def catch_all(request: Request, path: str):
|
||||
upstream_response = await call_official(request, path)
|
||||
|
Loading…
x
Reference in New Issue
Block a user