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
|
# 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]
|
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())
|
66
main_app.py
66
main_app.py
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user