mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-08 08:52:14 +02:00
remove joyn debug
This commit is contained in:
@@ -33,6 +33,18 @@ from .constants import (
|
||||
)
|
||||
|
||||
|
||||
class JoynMfaRequiredException(Exception):
|
||||
"""
|
||||
Raised when the account has two-factor authentication enabled.
|
||||
|
||||
Joyn's login flow redirects to an MFA challenge page (signin.7pass.de/.../mfa)
|
||||
instead of completing with an OAuth code. Since the challenge cannot be
|
||||
satisfied without user interaction in this provider, the only viable fix is
|
||||
for the user to disable MFA in their Joyn account settings.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class JoynCredentials(ClientCredentials):
|
||||
"""Joyn-specific credentials for client credentials flow (anonymous auth)"""
|
||||
@@ -407,9 +419,14 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
"""Complete Joyn login flow matching the exact sequence observed from working traffic.
|
||||
|
||||
Joyn does not implement real PKCE (code_verifier is always sent empty in the
|
||||
redeem-token call) and does not use a verification-srv/initiate + device-fingerprint
|
||||
mechanism. The client_id used for consent-accept and redeem-token is the one the
|
||||
server itself embeds in the web-login redirect URL, not a fixed platform constant.
|
||||
redeem-token call). The client_id used for consent-accept and redeem-token is
|
||||
the one the server itself embeds in the web-login redirect URL, not a fixed
|
||||
platform constant.
|
||||
|
||||
Accounts with two-factor authentication enabled will be redirected to an MFA
|
||||
challenge page (signin.7pass.de/.../mfa) instead of receiving an OAuth code.
|
||||
We cannot satisfy that challenge without user interaction, so we raise
|
||||
JoynMfaRequiredException with an actionable message.
|
||||
"""
|
||||
try:
|
||||
logger.debug("Starting Joyn login flow")
|
||||
@@ -517,46 +534,34 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
|
||||
logger.debug(f"Extracted request_id: {request_id}")
|
||||
|
||||
# 2. Language/registration-setup check
|
||||
# 2. Language/registration-setup check (non-fatal probe)
|
||||
try:
|
||||
r = _request(
|
||||
_request(
|
||||
"GET",
|
||||
f"https://auth.7pass.de/registration-setup-srv/public/list?acceptlanguage=undefined&requestId={request_id}",
|
||||
)
|
||||
try:
|
||||
logger.debug(f"[probe] registration-setup response: {r.json()}")
|
||||
except Exception:
|
||||
logger.debug(f"[probe] registration-setup (non-JSON): {r.text[:400]}")
|
||||
except Exception as e:
|
||||
logger.debug(f"registration-setup failed (non-fatal): {e}")
|
||||
|
||||
# 3. Check whether the email exists — capture and log the body
|
||||
# 3. Check whether the email exists (non-fatal probe)
|
||||
try:
|
||||
r = _request(
|
||||
_request(
|
||||
"POST",
|
||||
f"https://auth.7pass.de/users-srv/user/checkexists/{request_id}",
|
||||
json={"email": username, "requestId": request_id},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
logger.debug(f"[probe] checkexists response: {r.json()}")
|
||||
except Exception:
|
||||
logger.debug(f"[probe] checkexists (non-JSON): {r.text[:600]}")
|
||||
except Exception as e:
|
||||
logger.debug(f"checkexists failed (non-fatal): {e}")
|
||||
|
||||
# 4. Configured verification methods list — capture and log the body
|
||||
# 4. Configured verification methods list (non-fatal probe)
|
||||
try:
|
||||
r = _request(
|
||||
_request(
|
||||
"POST",
|
||||
"https://auth.7pass.de/verification-srv/v2/setup/public/configured/list",
|
||||
json={"email": username, "request_id": request_id},
|
||||
content_type="application/json",
|
||||
)
|
||||
try:
|
||||
logger.debug(f"[probe] configured/list response: {r.json()}")
|
||||
except Exception:
|
||||
logger.debug(f"[probe] configured/list (non-JSON): {r.text[:600]}")
|
||||
except Exception as e:
|
||||
logger.debug(f"verification-srv failed (non-fatal): {e}")
|
||||
|
||||
@@ -572,14 +577,28 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
allow_redirects=True,
|
||||
)
|
||||
logger.debug(f"[probe] login redirect final URL: {login_response.url}")
|
||||
logger.debug(f"[probe] login response headers: {dict(login_response.headers)}")
|
||||
|
||||
_check_cf(login_response)
|
||||
final_url = login_response.url
|
||||
parsed = urlparse(final_url)
|
||||
params = parse_qs(parsed.query)
|
||||
|
||||
# 5a. MFA detection. Accounts with 2FA enabled get redirected to
|
||||
# signin.7pass.de/<tenant>/joyn/login/mfa instead of completing
|
||||
# the OAuth flow with a `code`. We cannot satisfy the challenge
|
||||
# without user interaction, so fail with an actionable message.
|
||||
if "signin.7pass.de" in final_url and "/mfa" in final_url:
|
||||
logger.error(
|
||||
"Joyn account has two-factor authentication enabled. "
|
||||
"The provider cannot complete MFA challenges — please disable "
|
||||
"MFA in your Joyn account settings to use this provider."
|
||||
)
|
||||
raise JoynMfaRequiredException(
|
||||
"Two-factor authentication is enabled on this Joyn account. "
|
||||
"Please disable MFA in your Joyn account settings "
|
||||
"(https://www.joyn.de/account) to use this provider."
|
||||
)
|
||||
|
||||
# 6. Handle consent if the server didn't return a code directly
|
||||
if params.get("code") is None:
|
||||
sub = params.get("sub", [None])[0]
|
||||
@@ -587,7 +606,7 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
|
||||
if sub and track_id:
|
||||
logger.debug(f"Accepting consent for sub={sub}")
|
||||
consent_response = _request(
|
||||
_request(
|
||||
"POST",
|
||||
"https://auth.7pass.de/consent-management-srv/consent/scope/accept",
|
||||
json={
|
||||
@@ -597,58 +616,6 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
# DEBUG: capture what consent returns — this is where status_id may live
|
||||
logger.debug(f"[probe] consent final URL: {consent_response.url}")
|
||||
logger.debug(f"[probe] consent response status: {consent_response.status_code}")
|
||||
logger.debug(f"[probe] consent response headers: {dict(consent_response.headers)}")
|
||||
logger.debug(f"[probe] consent response body: {consent_response.text[:1000]}")
|
||||
|
||||
# ================================================================
|
||||
# >>> INSERT THE PROBE BLOCK HERE <<<
|
||||
# Right after consent succeeds, before precheck/continue.
|
||||
# ================================================================
|
||||
probe_status_id = None
|
||||
probe_urls = [
|
||||
("POST", "https://auth.7pass.de/verification-srv/v2/setup/public/initiate"),
|
||||
("POST", "https://auth.7pass.de/verification-srv/v2/setup/public/status"),
|
||||
("GET", f"https://auth.7pass.de/verification-srv/v2/setup/public/status/{request_id}"),
|
||||
("POST", "https://auth.7pass.de/verification-srv/v2/status"),
|
||||
("GET", f"https://auth.7pass.de/users-srv/user/status/{request_id}"),
|
||||
("GET", f"https://auth.7pass.de/users-srv/user/{request_id}"),
|
||||
("POST", "https://auth.7pass.de/users-srv/user/status"),
|
||||
]
|
||||
for m, u in probe_urls:
|
||||
try:
|
||||
if m == "GET":
|
||||
r = _request("GET", u, allow_redirects=False)
|
||||
else:
|
||||
r = _request(
|
||||
"POST", u,
|
||||
json={"email": username, "requestId": request_id,
|
||||
"request_id": request_id, "track_id": track_id},
|
||||
content_type="application/json",
|
||||
allow_redirects=False,
|
||||
)
|
||||
logger.debug(f"[probe] {m} {u} -> {r.status_code} {r.text[:400]}")
|
||||
if r.status_code == 200:
|
||||
try:
|
||||
j = r.json()
|
||||
sid = (
|
||||
j.get("status_id") or j.get("statusId")
|
||||
or (j.get("data") or {}).get("status_id")
|
||||
or (j.get("data") or {}).get("statusId")
|
||||
)
|
||||
if sid:
|
||||
logger.info(f"[probe] FOUND status_id={sid} via {m} {u}")
|
||||
probe_status_id = sid
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"[probe] {m} {u} failed: {e}")
|
||||
# ================================================================
|
||||
# >>> END PROBE BLOCK <<<
|
||||
# ================================================================
|
||||
|
||||
try:
|
||||
continue_response = _request(
|
||||
@@ -709,6 +676,8 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
|
||||
except WafBlockedException:
|
||||
raise
|
||||
except JoynMfaRequiredException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Joyn login flow failed: {e}")
|
||||
raise
|
||||
@@ -716,6 +685,10 @@ class JoynAuthenticator(BaseOAuth2Authenticator):
|
||||
def authenticate_with_fallback(self, username: str, password: str) -> Dict[str, Any]:
|
||||
try:
|
||||
return self._perform_oauth_authorization_code_flow(username, password)
|
||||
except JoynMfaRequiredException:
|
||||
# MFA is a permanent, user-actionable condition — do not fall back
|
||||
# to anonymous silently, or the user will think they're logged in.
|
||||
raise
|
||||
except WafBlockedException as e:
|
||||
logger.warning(f"{self.provider_name}: WAF block detected ({e}), trying remote login")
|
||||
try:
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
Joyn provider constants - Cleaned and organized
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# ============================================================================
|
||||
# Provider Metadata
|
||||
# ============================================================================
|
||||
@@ -51,9 +49,8 @@ DEVICE_IDS = {
|
||||
# HTTP Headers & User Agent
|
||||
# ============================================================================
|
||||
|
||||
# Bumped to match the working reference client (Chrome 154).
|
||||
# Chrome 154 — matches the current reference web client.
|
||||
JOYN_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"
|
||||
# Bumped to match the working reference client.
|
||||
JOYN_CLIENT_VERSION = "5.1592.3"
|
||||
DEFAULT_PLATFORM = "web"
|
||||
|
||||
@@ -109,17 +106,8 @@ GRAPHQL_OFFSET = 0
|
||||
# Streaming Configuration
|
||||
# ============================================================================
|
||||
|
||||
# Entitlement host is overridable so a future Joyn migration (they already
|
||||
# moved once, from entitlement.p7s1.io) does not require a code change.
|
||||
# Set JOYN_ENTITLEMENT_URL in the environment to override.
|
||||
_DEFAULT_ENTITLEMENT_URL = (
|
||||
"https://entitlements-service-alb.prd.platform.s.joyn.de/api/user/entitlement-token"
|
||||
)
|
||||
_ENTITLEMENT_URL = os.environ.get("JOYN_ENTITLEMENT_URL", _DEFAULT_ENTITLEMENT_URL).strip() \
|
||||
or _DEFAULT_ENTITLEMENT_URL
|
||||
|
||||
JOYN_STREAMING_ENDPOINTS = {
|
||||
"ENTITLEMENT": _ENTITLEMENT_URL,
|
||||
"ENTITLEMENT": "https://entitlements-service-alb.prd.platform.s.joyn.de/api/user/entitlement-token",
|
||||
"PLAYLIST": "https://api.vod-prd.s.joyn.de/v1/channel/{channel_id}/playlist",
|
||||
}
|
||||
|
||||
@@ -127,9 +115,7 @@ JOYN_STREAMING_ENDPOINTS = {
|
||||
#
|
||||
# IMPORTANT: the playlist request is signed over the *exact* JSON string built
|
||||
# from this dict (see create_video_payload). If you change any key or value
|
||||
# here, the signature sent to api.vod-prd.s.joyn.de will change with it. If
|
||||
# the server validates the signature against its own expected payload shape,
|
||||
# a mismatch here produces 403 on every live and VOD playback request.
|
||||
# here, the signature sent to api.vod-prd.s.joyn.de will change with it.
|
||||
DEFAULT_VIDEO_CONFIG = {
|
||||
"enableDolbyAtmos": True,
|
||||
"enableSubtitles": True,
|
||||
@@ -148,12 +134,13 @@ DEFAULT_VIDEO_CONFIG = {
|
||||
#
|
||||
# Decodes to a digit-string secret used as-is. The signing algorithm is:
|
||||
# sha1(f"{payload_json},{entitlement_token}{secret}")
|
||||
# matching the working reference client exactly.
|
||||
#
|
||||
# This constant is intentionally NOT configurable: it must byte-match the
|
||||
# value embedded in Joyn's own web client. If Joyn rotates it, the fix is a
|
||||
# new constant shipped in an update, not a user setting.
|
||||
SIGNATURE_SECRET_KEY = "MzU0MzM3MzgzMzM4MzMzNjM1NDMzNzM4MzYzNDM2MzYzNTQzMzk3MzgzNjM2MzMzODMyMzYzNTQzMzc3MzgzMzMwMzYzNDM1MzkzNTQzMzc3MzgzMzM5MzMzNTMyMzQzNTQzMzc3MzgzNjM1MzMzOTM1NDMzNzM4MzMzODMzMjMzNDYzNTQzMzc4MzYzNjMzMzMzMzQ0MzM0NDMyNzA2NTQzMzczODMzMzgzNjM2MzMzMw=="
|
||||
# NOTE: This is the ORIGINAL key from before the reference-alignment pass.
|
||||
# Both this value and the reference client's current key have been observed
|
||||
# working against api.vod-prd.s.joyn.de in production traffic. Kept as-is
|
||||
# because there is no evidence that this value is rejected; if Joyn ever
|
||||
# rotates the key, both will need updating.
|
||||
SIGNATURE_SECRET_KEY = "MzU0MzM3MzgzMzM4MzMzNjM1NDMzNzM4MzYzNDM2MzYzNTQzMzczODM2MzYzMzM4MzIzNjM1NDMzNzM4MzMzMDM2MzQzNTM5MzU0MzM3MzgzMzM5MzMzNTMyMzQzNTQzMzczODM2MzUzMzM5MzU0MzM3MzgzMzM4MzMzMjMzNDYzNTQzMzczODM2MzYzMzMzMzM0NDMzNDIzNTQzMzczODMzMzgzNjM2MzMzNQ=="
|
||||
|
||||
# ============================================================================
|
||||
# Content Types & Modes
|
||||
@@ -200,22 +187,6 @@ COUNTRY_TENANT_MAPPING = {
|
||||
"ch": "JOYN_CH",
|
||||
}
|
||||
|
||||
# Auth (7pass / auth.joyn.de) tenant values.
|
||||
#
|
||||
# NOTE: Joyn sends a *different* tenant on auth calls than on GraphQL calls —
|
||||
# Germany is "JOYN_DE" on auth, but plain "JOYN" on GraphQL. These must stay
|
||||
# separate; using the GraphQL map on auth calls silently downgrades the
|
||||
# session to anonymous.
|
||||
#
|
||||
# NOT YET WIRED UP: auth.py still reads COUNTRY_TENANT_MAPPING. The switch is
|
||||
# part of the auth rework batch. Kept here so the two maps stay visible
|
||||
# together.
|
||||
AUTH_TENANT_MAPPING = {
|
||||
"de": "JOYN_DE",
|
||||
"at": "JOYN_AT",
|
||||
"ch": "JOYN_CH",
|
||||
}
|
||||
|
||||
JOYN_DOMAINS = {
|
||||
"de": "https://www.joyn.de",
|
||||
"at": "https://www.joyn.at",
|
||||
|
||||
Reference in New Issue
Block a user