refactored

This commit is contained in:
Pari Malam
2026-05-30 10:52:45 +08:00
parent 9345cd8501
commit ffa26cd8bf
18 changed files with 4505 additions and 4119 deletions
+1
View File
@@ -0,0 +1 @@
from __future__ import annotations
+386
View File
@@ -0,0 +1,386 @@
from __future__ import annotations
import json
import random
import sys
import time
from pathlib import Path
from typing import Optional
from urllib.parse import quote
from pywidevine import Cdm as WidevineCdm, Device as WidevineDevice
from modules.msl.android import MSL_ANDROID
from modules.helpers import (
ensure_output_dir, restore_auth_cookies, get_nfvdid, get_flow_session_cookies,
save_session_cookies,
generate_netflix_uuid, generate_request_id, generate_esn_random_suffix,
decrypt_msl_header, extract_clcs_session_id, extract_rendition_id,
)
from modules.config import setup_config
from modules.logging import setup_logger
from modules.session import setup_session
config = setup_config()
EMAIL = config["NETFLIX"]["EMAIL"]
PASSWORD = config["NETFLIX"]["PASSWORD"]
def run_android(wvd_path: Path,
new_msl: bool = False, no_verify: bool = False,
proxy: Optional[str] = None):
log = setup_logger('ANDROID MSL')
OUTPUT_DIR = ensure_output_dir("android")
NETFLIX_HOME_URL = "https://www.netflix.com/"
NETFLIX_CANONICAL_URL = "https://netflix.com/"
LOGIN_URL = "https://www.netflix.com/login"
APPBOOT_URL = "https://android15.appboot.netflix.com/appboot/NFANDROID1-PRV-P-"
MSL_HANDSHAKE_ENDPOINT = "https://android.prod.ftl.netflix.com/nq/androidui/pbo_license/~1.0.0/router"
VERIFY_LOGIN_URL = "https://android.prod.ftl.netflix.com/nq/androidui/samurai/v1/config"
USER_AGENT = "com.netflix.mediaclient/63988 (Linux; U; Android 15; en_US; SM-F711N; Build/AP3A.240905.015.A2; Cronet/143.0.7445.0)"
CLIENT_VERSION = "18.26.0"
APP_VERSION = "9.60.0"
HAWKINS_VERSION = "5.15.0"
UI_FLAVOR = "android"
OS_VERSION = "35"
FORM_FACTOR = "phone"
FEATURE_CAPABILITIES = "supportsStudioBranding"
LOCALE = "en-US"
DEVICE_MODEL = "SM-F711N"
VERIFY_TLS = not no_verify
RESTORE_AUTH_COOKIES = False
if not wvd_path.exists():
raise FileNotFoundError(f"Missing WVD file: {wvd_path}")
widevine_device = WidevineDevice.load(wvd_path)
cdm = WidevineCdm.from_device(widevine_device)
_sid = widevine_device.system_id
MSL_CACHE_PATH = OUTPUT_DIR / f"msl_keys_cache_android_{_sid}.json"
AUTH_COOKIES_PATH = OUTPUT_DIR / f"netflix_auth_cookies_{_sid}.json"
USERIDTOKEN_PATH = OUTPUT_DIR / f"netflix_auth_useridtoken_{_sid}.json"
TOKENS_OUTPUT_PATH = OUTPUT_DIR / f"netflix_auth_tokens_{_sid}.json"
ESN = f"NFANDROID1-PRV-P-SAMSUSM-F711N-{_sid}-{generate_esn_random_suffix(64)}"
log.info("ESN: %s", ESN)
REQUEST_CLIENT_CONTEXT_UNKNOWN = '{"appView":"unknown","appState":"foreground"}'
APPBOOT_REQUEST_CLIENT_CONTEXT = '{"appView":"unknown","appState":"foreground"}'
session = setup_session(verify_tls=VERIFY_TLS, proxy=proxy)
_proxy = {"http": proxy, "https": proxy} if proxy else None
if RESTORE_AUTH_COOKIES:
restore_auth_cookies(session, AUTH_COOKIES_PATH, log)
log.info("Initializing session")
response = session.get(NETFLIX_CANONICAL_URL, timeout=30, allow_redirects=True)
response.raise_for_status()
response = session.get(NETFLIX_HOME_URL, timeout=30)
response.raise_for_status()
log.info("Requesting initial nfvdid cookie")
appboot_headers = {
"Host": "android15.appboot.netflix.com",
"Connection": "keep-alive",
"X-Netflix.Request.Client.Context": APPBOOT_REQUEST_CLIENT_CONTEXT,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": USER_AGENT,
"Accept-Encoding": "gzip, deflate, br",
}
response = session.post(
APPBOOT_URL,
params={"keyVersion": "1"},
headers=appboot_headers,
timeout=30,
)
nfvdid = get_nfvdid(session, response)
log.info("Initial nfvdid cookie obtained")
log.info("Starting MSL Widevine exchange")
msl_headers = MSL_ANDROID.build_request_headers(
request_name="getProxyEsn",
user_agent=USER_AGENT,
referer=None,
esn=ESN,
expiry_timeout=12750,
host="android15.prod.cloud.netflix.com",
language="en-US,en",
device_model=quote(DEVICE_MODEL, safe=""),
extra_headers={
"Accept-Encoding": "gzip, deflate, br",
"Content-Encoding": "msl_v1",
"x-netflix.zuul.brotli.allowed": "true",
"x-netflix.appver": APP_VERSION,
"x-netflix.clienttype": "samurai",
"x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT_UNKNOWN,
"x-netflix.esnprefix": "NFANDROID1-PRV-P-",
"x-netflix.request.uuid": (
generate_netflix_uuid()
),
"x-netflix.androidapi": "35",
"x-netflix.deviceformfactor": "PHONE",
"x-netflix.devicememorylevel": "HIGH",
"x-netflix.request.attempt": "1",
"x-netflix.request.id": generate_request_id(),
"Content-Type": "application/json",
"x-netflix.client.request.name": "getProxyEsn",
"x-netflix.request.routing": '{"path":"\\/nq\\/android\\/playback\\/~1.0.0\\/router"}',
"user-agent": USER_AGENT,
},
)
handshake_cookies = {
"nfvdid": nfvdid,
}
msl_keys = MSL_ANDROID.handshake(
msl_keys_path=str(MSL_CACHE_PATH),
session=session,
sender=ESN,
cdm=cdm,
cdm_device=str(wvd_path),
new_msl=False,
cookies=handshake_cookies,
drm="widevine",
endpoint=MSL_HANDSHAKE_ENDPOINT,
headers=msl_headers,
)
msl_client = MSL_ANDROID(
session=session,
keys=msl_keys,
message_id=random.randint(0, 2**52),
sender=ESN,
drm="widevine",
proxy=_proxy,
)
nfvdid, flow_session_id = get_flow_session_cookies(session)
log.info("MSL Widevine exchange completed")
log.info("Loading login page")
response = session.get(LOGIN_URL, timeout=30)
response.raise_for_status()
login_html = response.text
cookie_dict = session.cookies.get_dict()
flow_session_id = cookie_dict.get("flwssn", flow_session_id)
if not flow_session_id:
raise RuntimeError("The flwssn flow session cookie is missing")
log.info("Submitting VerifyLoginMslRequest")
confirm_login_query = {
"api": "33",
"appType": "samurai",
"appVer": "62902",
"appVersion": "9.18.0",
"chipset": "sm8150",
"chipsetHardware": "qcom",
"clientAppState": "FOREGROUND",
"clientAppVersionState": "NORMAL",
"countryIsoCode": "US",
"ctgr": "phone",
"dbg": "false",
"deviceLocale": "en-US",
"devmod": f"samsung_{DEVICE_MODEL}",
"ffbc": "phone",
"flwssn": flow_session_id,
"installType": "regular",
"isAutomation": "false",
"isConsumptionOnly": "true",
"isNetflixPreloaded": "false",
"isPlayBillingEnabled": "true",
"isStubInSystemPartition": "false",
"lackLocale": "false",
"landingOrigin": "https://www.netflix.com",
"mId": "SAMSUSM-F711N",
"memLevel": "HIGH",
"method": "get",
"mnf": "samsung",
"model": DEVICE_MODEL,
"netflixClientPlatform": "androidNative",
"netflixId": cookie_dict.get("NetflixId", ""),
"networkType": "wifi",
"osBoard": "kona",
"osDevice": "bloom",
"osDisplay": "RP1A.200720.012",
"password": PASSWORD,
"path": '["signInVerify"]',
"pathFormat": "hierarchical",
"platform": "android",
"preloadSignupRoValue": "",
"progressive": "false",
"qlty": "hd",
"recaptchaResponseTime": 445,
"recaptchaResponseToken": "",
"responseFormat": "json",
"roBspVer": "RP1A.200720.012",
"secureNetflixId": cookie_dict.get("SecureNetflixId", ""),
"sid": "7176",
"store": "google",
"userLoginId": EMAIL,
}
confirm_login_headers = {
"X-Netflix.Request.NqTracking": "VerifyLoginMslRequest",
"X-Netflix.Client.Request.Name": "VerifyLoginMslRequest",
"X-Netflix.Request.Client.Context": '{"appState":"foreground"}',
"X-Netflix-Esn": ESN,
"X-Netflix.EsnPrefix": "NFANDROID1-PRV-P-",
"X-Netflix.msl-header-friendly-client": "true",
"content-encoding": "msl_v1",
}
_THROTTLE_RETRIES = 3
_THROTTLE_WAIT = 60
_clcs_attempted = False
for _attempt in range(1, _THROTTLE_RETRIES + 2): # +1 slot for the CLCS retry
try:
confirm_login_header, confirm_login_payload_chunks = msl_client.send_message(endpoint=VERIFY_LOGIN_URL,
params=confirm_login_query,
application_data={},
headers=confirm_login_headers)
except Exception:
log.error("VerifyLoginMslRequest failed")
log.debug("Request URL: %s", VERIFY_LOGIN_URL)
log.debug("Request params: %s", json.dumps(confirm_login_query, indent=2))
log.debug("Request headers: %s", json.dumps(confirm_login_headers, indent=2))
log.debug("Session cookies: %s", json.dumps(session.cookies.get_dict(), indent=2))
log.exception("Exception occurred")
sys.exit(1)
_error_code = None
if (
isinstance(confirm_login_payload_chunks, dict)
and "errorCode" in confirm_login_payload_chunks.get("jsonGraph", {}).get("signInVerify", {}).get("value", {}).get("fields", {})
):
_error_code = (
confirm_login_payload_chunks.get("jsonGraph", {})
.get("signInVerify", {})
.get("value", {})
.get("fields", {})
.get("errorCode", {})
.get("value")
)
if _error_code == "throttling_failure" and _attempt < _THROTTLE_RETRIES:
log.warning("Throttled by Netflix (attempt %d/%d), retrying in %ds...", _attempt, _THROTTLE_RETRIES, _THROTTLE_WAIT)
time.sleep(_THROTTLE_WAIT)
continue
elif _error_code == "incorrect_password" and not _clcs_attempted:
# Samurai rejected credentials without auth cookies — fall back to CLCS
# web login to obtain NetflixId/SecureNetflixId, then retry once.
log.warning("incorrect_password — falling back to CLCS web login")
clcs_session_id = extract_clcs_session_id(login_html)
rendition_id = extract_rendition_id(login_html)
if not clcs_session_id or not rendition_id:
log.error("Cannot extract CLCS session IDs from login page for fallback")
sys.exit(1)
clcs_resp = session.post(
"https://web.prod.cloud.netflix.com/graphql",
json={
"operationName": "CLCSScreenUpdate",
"variables": {
"format": "HTML",
"imageFormat": "PNG",
"locale": "en-US",
"serverState": json.dumps({
"realm": "growth",
"name": "PASSWORD_LOGIN",
"clcsSessionId": clcs_session_id,
"sessionContext": {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
"login.navigationSettings": {"hideOtpToggle": True},
},
}, separators=(",", ":")),
"serverScreenUpdate": json.dumps({
"realm": "custom",
"name": "growthLoginByPassword",
"metadata": {"recaptchaSiteKey": "6Lf8hrcUAAAAAIpQAFW2VFjtiYnThOjZOA5xvLyR"},
"loggingAction": "Submitted",
"loggingCommand": "SubmitCommand",
"referrerRenditionId": rendition_id,
}, separators=(",", ":")),
"inputFields": [
{"name": "password", "value": {"stringValue": PASSWORD}},
{"name": "userLoginId", "value": {"stringValue": EMAIL}},
{"name": "countryCode", "value": {"stringValue": "1"}},
{"name": "countryIsoCode", "value": {"stringValue": "US"}},
{"name": "recaptchaResponseTime", "value": {"intValue": 445}},
{"name": "recaptchaResponseToken", "value": {"stringValue": ""}},
],
},
"extensions": {"persistedQuery": {"id": "1c276cdf-caef-49cf-b38e-384972c2b47e", "version": 102}},
},
headers={
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": LOGIN_URL,
},
timeout=30,
)
if "errors" in clcs_resp.json():
log.error("CLCS fallback login failed: %s", clcs_resp.json().get("errors"))
sys.exit(1)
session.get("https://www.netflix.com/browse", timeout=30)
_fresh = session.cookies.get_dict()
confirm_login_query["netflixId"] = _fresh.get("NetflixId", "")
confirm_login_query["secureNetflixId"] = _fresh.get("SecureNetflixId", "")
confirm_login_query["flwssn"] = _fresh.get("flwssn", flow_session_id)
_clcs_attempted = True
log.info("CLCS fallback complete, retrying VerifyLoginMslRequest")
continue
elif _error_code:
log.error("Login errorCode: %s", _error_code)
sys.exit(1)
break
if "headerdata" not in confirm_login_header:
log.critical("Missing 'headerdata' in MSL response")
sys.exit(1)
try:
header_data = decrypt_msl_header(confirm_login_header["headerdata"], msl_client.keys.encryption, msl_client.keys.sign)
except Exception:
log.exception("Failed to decrypt MSL header")
sys.exit(1)
tokens = header_data.get("useridtoken")
if not tokens:
log.error("Authentication failed: invalid ESN, email, or password")
sys.exit(1)
try:
TOKENS_OUTPUT_PATH.write_text(json.dumps(header_data, indent=4), encoding="utf-8")
USERIDTOKEN_PATH.write_text(json.dumps(tokens, indent=2), encoding="utf-8")
log.info("User ID token data saved to: %s", TOKENS_OUTPUT_PATH)
log.info("User ID token saved to: %s", USERIDTOKEN_PATH)
except Exception:
log.exception("Failed to save token files")
sys.exit(1)
try:
auth_cookies = save_session_cookies(session, AUTH_COOKIES_PATH, log)
except Exception:
sys.exit(1)
result = {
"useridtoken": tokens,
"auth_cookies": auth_cookies,
"header_data": header_data,
}
log.info("VerifyLoginMslRequest succeeded")
# print(json.dumps(result, indent=2))
+201
View File
@@ -0,0 +1,201 @@
from __future__ import annotations
import json
import random
import re
import sys
from pathlib import Path
from typing import Optional
from urllib.parse import quote
from modules.msl.android import MSL_ANDROID
from modules.helpers import (
ensure_output_dir, get_nfvdid,
generate_hex_id,
)
from modules.config import setup_config
from modules.logging import setup_logger
from modules.session import setup_session
config = setup_config()
EMAIL = config["NETFLIX"]["EMAIL"]
PASSWORD = config["NETFLIX"]["PASSWORD"]
def run_android_rsa(new_msl: bool = False, no_verify: bool = False,
proxy: Optional[str] = None):
logger = setup_logger('ANDROID MSL RSA')
output_dir = ensure_output_dir("android")
msl_cache_path = output_dir / "msl_keys_cache_android_rsa.json"
auth_cookies_path = output_dir / "netflix_auth_cookies_rsa.json"
useridtoken_path = output_dir / "netflix_auth_useridtoken_rsa.json"
tokens_output_path = output_dir / "netflix_auth_tokens_rsa.json"
# NFCDCH-02-* ESN is accepted by the Android FTL endpoint without a WVD
esn = f"NFCDCH-02-{''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(32))}"
user_agent = f"com.netflix.mediaclient/63988 (Linux; U; Android 15; en_US; SM-F711N; Build/AP3A.240905.015.A2; Cronet/143.0.7445.0)"
device_model = "SM-F711N"
session = setup_session(verify_tls=not no_verify, proxy=proxy)
_proxy = {"http": proxy, "https": proxy} if proxy else None
response = session.post(
"https://android15.appboot.netflix.com/appboot/NFANDROID1-PRV-P-",
params={"keyVersion": "1"},
headers={
"Host": "android15.appboot.netflix.com",
"X-Netflix.Request.Client.Context": '{"appView":"unknown","appState":"foreground"}',
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": user_agent,
"Accept-Encoding": "gzip, deflate, br",
},
timeout=30,
)
nfvdid = get_nfvdid(session, response)
logger.info("Initial nfvdid cookie obtained")
msl_headers = MSL_ANDROID.build_request_headers(
request_name="getProxyEsn",
user_agent=user_agent,
referer=None,
esn=esn,
expiry_timeout=12750,
host="android15.prod.cloud.netflix.com",
language="en-US,en",
device_model=quote(device_model, safe=""),
extra_headers={
"Accept-Encoding": "gzip, deflate, br",
"Content-Encoding": "msl_v1",
"x-netflix.zuul.brotli.allowed": "true",
"x-netflix.appver": "9.60.0",
"x-netflix.clienttype": "samurai",
"x-netflix.request.client.context": '{"appView":"unknown","appState":"foreground"}',
"x-netflix.esnprefix": "NFANDROID1-PRV-P-",
"x-netflix.request.uuid": f"{generate_hex_id(8)}-{generate_hex_id(4)}-{generate_hex_id(4)}-{generate_hex_id(4)}-{generate_hex_id(12)}",
"x-netflix.androidapi": "35",
"x-netflix.deviceformfactor": "PHONE",
"x-netflix.devicememorylevel": "HIGH",
"x-netflix.request.attempt": "1",
"x-netflix.request.id": generate_hex_id(32),
"Content-Type": "application/json",
"x-netflix.client.request.name": "getProxyEsn",
"x-netflix.request.routing": '{"path":"\\/nq\\/android\\/playback\\/~1.0.0\\/router"}',
"user-agent": user_agent,
},
)
logger.info("Performing RSA/ASYMMETRIC_WRAPPED MSL handshake (no WVD needed)")
msl_keys = MSL_ANDROID.rsa_handshake(
msl_keys_path=str(msl_cache_path),
session=session,
sender=esn,
new_msl=new_msl,
cookies={"nfvdid": nfvdid},
endpoint="https://android.prod.ftl.netflix.com/nq/androidui/pbo_license/~1.0.0/router",
headers=msl_headers,
)
msl_client = MSL_ANDROID(
session=session,
keys=msl_keys,
message_id=random.randint(0, 2**52),
sender=esn,
drm="widevine",
proxy=_proxy,
)
logger.info("MSL RSA key exchange completed")
# The NFCDCH-02-* ESN triggers the web CLCS auth flow (not samurai useridtoken).
# After the MSL handshake the HTTP session carries Netflix cookies, so we use
# the same CLCSScreenUpdate GraphQL path that run_web() uses.
logger.info("Fetching login page and extracting CLCS session context")
login_response = session.get("https://www.netflix.com/login", timeout=30)
login_html = login_response.text
clcs_session_id = None
rendition_id = None
patterns = [
r'clcsSessionId[\\"\'": ]+([0-9a-f\-]{36})',
r'(?<!clcs)renditionId[\\"\'": ]+([0-9a-f\-]{36})',
]
for pattern in patterns:
match = re.search(pattern, login_html)
if match:
if not clcs_session_id:
clcs_session_id = match.group(1)
elif not rendition_id:
rendition_id = match.group(1)
if not clcs_session_id or not rendition_id:
logger.error("Could not extract CLCS session IDs from login page")
sys.exit(1)
logger.info("Submitting credentials via CLCSScreenUpdate (web flow)")
full_variables = {
"format": "HTML", "imageFormat": "PNG", "locale": "en-US",
"serverState": json.dumps({
"realm": "growth", "name": "PASSWORD_LOGIN",
"clcsSessionId": clcs_session_id,
"sessionContext": {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
"login.navigationSettings": {"hideOtpToggle": True},
},
}, separators=(",", ":")),
"serverScreenUpdate": json.dumps({
"realm": "custom", "name": "growthLoginByPassword",
"metadata": {"recaptchaSiteKey": "6Lf8hrcUAAAAAIpQAFW2VFjtiYnThOjZOA5xvLyR"},
"loggingAction": "Submitted", "loggingCommand": "SubmitCommand",
"referrerRenditionId": rendition_id,
}, separators=(",", ":")),
"inputFields": [
{"name": "password", "value": {"stringValue": PASSWORD}},
{"name": "userLoginId", "value": {"stringValue": EMAIL}},
{"name": "countryCode", "value": {"stringValue": "1"}},
{"name": "countryIsoCode", "value": {"stringValue": "US"}},
{"name": "recaptchaResponseTime", "value": {"intValue": 445}},
{"name": "recaptchaResponseToken", "value": {"stringValue": ""}},
],
}
response = session.post(
"https://web.prod.cloud.netflix.com/graphql",
json={
"operationName": "CLCSScreenUpdate",
"variables": full_variables,
"extensions": {"persistedQuery": {"id": "1c276cdf-caef-49cf-b38e-384972c2b47e", "version": 102}},
},
headers={
"User-Agent": user_agent,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": "https://www.netflix.com/login",
},
timeout=30,
)
login_resp_json = response.json()
if "errors" in login_resp_json:
logger.error("CLCSScreenUpdate failed: %s", login_resp_json["errors"])
sys.exit(1)
# Check login result
data = login_resp_json.get("data", {})
result_block = data.get("result", {}) if isinstance(data, dict) else {}
status = result_block.get("status") if isinstance(result_block, dict) else None
# Finalise the session
session.get("https://www.netflix.com/browse", timeout=30)
auth_cookies = {cookie.name: cookie.value for cookie in session.cookies}
auth_cookies_path.write_text(json.dumps(auth_cookies, indent=2), encoding="utf-8")
logger.info("Authentication cookies saved")
has_netflix_id = "NetflixId" in auth_cookies
if status == "SUCCESS" or has_netflix_id:
logger.info("LOGIN SUCCESSFUL")
tokens_output_path.write_text(json.dumps(login_resp_json, indent=4), encoding="utf-8")
result = {"status": "SUCCESS", "auth_cookies": auth_cookies}
# print(json.dumps(result, indent=2))
else:
logger.error("LOGIN FAILED — status=%s cookies=%s", status, list(auth_cookies.keys()))
sys.exit(1)
+336
View File
@@ -0,0 +1,336 @@
from __future__ import annotations
import json
import random
import sys
from typing import Any, Dict, Optional
from pathlib import Path
from urllib.parse import quote
from pywidevine import Cdm as WidevineCdm, Device as WidevineDevice
from modules.msl.ios import MSL_IOS
from modules.helpers import (
ensure_output_dir,
get_nfvdid, get_flow_session_cookies,
save_session_cookies,
generate_request_id, generate_esn_random_suffix,
decrypt_msl_header,
extract_clcs_session_id, extract_rendition_id,
)
from modules.config import setup_config
from modules.logging import setup_logger
from modules.session import setup_session
config = setup_config()
EMAIL = config["NETFLIX"]["EMAIL"]
PASSWORD = config["NETFLIX"]["PASSWORD"]
def run_ios(wvd_path: Path,
new_msl: bool = False, no_verify: bool = False,
proxy: Optional[str] = None):
log = setup_logger('IOS MSL')
OUTPUT_DIR = ensure_output_dir("ios")
NETFLIX_HOME_URL = "https://www.netflix.com/"
NETFLIX_CANONICAL_URL = "https://netflix.com/"
GRAPHQL_URL = "https://ios.prod.cloud.netflix.com/graphql"
LOGIN_URL = "https://www.netflix.com/login"
BROWSE_URL = "https://www.netflix.com/browse"
APPBOOT_URL = "https://ios18.appboot.netflix.com/appboot/NFANDROID1-PRV-P-"
MSL_HANDSHAKE_ENDPOINT = "https://ios.prod.ftl.netflix.com/nq/iosplatform/pbo_license/~1.0.0/router"
USER_AGENT = "Netflix/5850 CFNetwork/3826.600.41 Darwin/24.6.0"
CLIENT_VERSION = "18.26.0"
APP_VERSION = "18.26.0"
HAWKINS_VERSION = "5.16.0"
UI_FLAVOR = "argo"
OS_VERSION = "18.6.2"
FORM_FACTOR = "phone"
FEATURE_CAPABILITIES = "supportsStudioBranding"
LOCALE = "en-US"
DEVICE_MODEL = "iPhone15,3"
if not wvd_path.exists():
raise FileNotFoundError(f"Missing WVD file: {wvd_path}")
device = WidevineDevice.load(wvd_path)
cdm = WidevineCdm.from_device(device)
_sid = device.system_id
MSL_CACHE_PATH = OUTPUT_DIR / f"msl_keys_cache_ios_{_sid}.json"
AUTH_COOKIES_PATH = OUTPUT_DIR / f"netflix_auth_cookies_{_sid}.json"
ESN = f"NFANDROID1-PRV-P-IPHONE15=3-{_sid}-{generate_esn_random_suffix(64)}"
log.info("ESN: %s", ESN)
REQUEST_CLIENT_CONTEXT_LANDING = '{"appView":"nmLanding","appState":"foreground"}'
REQUEST_CLIENT_CONTEXT_IDENTIFIER = '{"appView":"login","appState":"foreground"}'
REQUEST_CLIENT_CONTEXT_PASSWORD = '{"appView":"passwordLogin","appState":"foreground"}'
REQUEST_CLIENT_CONTEXT_PROFILES = '{"appView":"profilesGate","appState":"foreground"}'
APPBOOT_CLIENT_CONTEXT = '{"appState":"foreground","reason":"user-action"}'
APPBOOT_REQUEST_CLIENT_CONTEXT = '{"appView":"unknown","appState":"foreground"}'
RECAPTCHA_SITE_KEY = "6Lf8hrcUAAAAAIpQAFW2VFjtiYnThOjZOA5xvLyR"
QUERY_IDS = {
"MembershipStatus": {"id": "3f50f3b3-fff8-48c0-bbd3-5fa2cb04b3c1", "version": 102},
"CLCSScreenUpdate": {"id": "1c276cdf-caef-49cf-b38e-384972c2b47e", "version": 102},
"CLCSSendFeedback": {"id": "079b2271-196b-4edd-b65c-e9439b22e305", "version": 102},
"CLCSInterstitialProfileGate": {"id": "b6e10c7d-0e6f-4921-83b5-177995a80d97", "version": 102},
}
recaptcha_token = ""
verify_tls = not no_verify
restore_auth_cookies = False
session = setup_session(verify_tls=verify_tls, proxy=proxy)
_proxy = {"http": proxy, "https": proxy} if proxy else None
if restore_auth_cookies:
restore_auth_cookies(session, AUTH_COOKIES_PATH, log)
log.info("Initializing session")
response = session.get(NETFLIX_CANONICAL_URL, timeout=30, allow_redirects=True)
response.raise_for_status()
response = session.get(NETFLIX_HOME_URL, timeout=30)
response.raise_for_status()
log.info("Requesting initial nfvdid cookie")
appboot_request_id = generate_request_id()
appboot_headers = {
"Host": "ios18.appboot.netflix.com",
"X-Netflix.Client.appVersion": APP_VERSION,
"Accept": "*/*",
"X-Netflix.Request.Id": appboot_request_id,
"X-Netflix.APIAction": "appboot",
"X-Netflix.Client.Context": APPBOOT_CLIENT_CONTEXT,
"X-Netflix.Client.Request.Name": "appboot",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded",
"X-Netflix.Request.Attempt": "1",
"X-Netflix.Request.Client.Context": APPBOOT_REQUEST_CLIENT_CONTEXT,
"User-Agent": USER_AGENT,
}
response = session.post(
APPBOOT_URL,
params={"keyVersion": "1"},
headers=appboot_headers,
timeout=30,
)
nfvdid = get_nfvdid(session, response)
log.info("Initial nfvdid cookie obtained")
log.info("Starting MSL Widevine exchange")
msl_headers = MSL_IOS.build_request_headers(
request_name="mintCookies",
user_agent=USER_AGENT,
referer=None,
esn=ESN,
expiry_timeout=12750,
host="ios.prod.ftl.netflix.com",
language="en-US,en",
device_model=quote(DEVICE_MODEL, safe=""),
extra_headers={
"Accept-Encoding": "deflate,gzip",
"Accept-Language": "en-US,en;q=0.9",
"Content-Encoding": "msl_v1",
"X-Gibbon-Cache-Control": "no-cache",
"X-AllowCompression": "true",
"X-Client-Request-Id": str(random.randint(10**17, 10**18 - 1)),
"X-DeviceModel": quote(DEVICE_MODEL, safe=""),
"x-netflix.esn": ESN,
"X-Netflix.Client.Request.Name": "mintCookies",
"X-Netflix.Request.Client.Context": '{"appView":"login","appState":"foreground"}',
},
)
handshake_cookies = {
"nfvdid": nfvdid,
}
msl_keys = MSL_IOS.handshake(
msl_keys_path=str(MSL_CACHE_PATH),
session=session,
sender=ESN,
cdm=cdm,
cdm_device=str(wvd_path),
new_msl=False,
cookies=handshake_cookies,
drm="widevine",
endpoint=MSL_HANDSHAKE_ENDPOINT,
headers=msl_headers,
)
msl_client = MSL_IOS(
session=session,
keys=msl_keys,
message_id=random.randint(0, pow(2, 52)),
sender=ESN,
drm="widevine",
proxy=_proxy,
)
nfvdid, flow_session_id = get_flow_session_cookies(session)
log.info("MSL Widevine exchange completed")
log.info("Submitting membership request")
operation_name = "MembershipStatus"
variables = {}
headers = {
"Host": "ios.prod.cloud.netflix.com",
"Connection": "keep-alive",
"X-Netflix.Request.Client.Context": REQUEST_CLIENT_CONTEXT_LANDING,
"Content-Encoding": "msl_v1",
"x-netflix.context.feature-capabilities": FEATURE_CAPABILITIES,
"x-netflix.context.operation-name": operation_name,
"X-Netflix.request.expiry.timeout": "15000",
"X-Netflix.Request.Id": generate_request_id(),
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.form-factor": FORM_FACTOR,
"X-Netflix.Request.Attempt": "1",
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept-Language": "en-US,en;q=0.9",
"Accept": "*/*",
"Content-Type": "application/json",
"x-netflix.context.locales": LOCALE,
"x-netflix.context.os-version": OS_VERSION,
"Accept-Encoding": "gzip, deflate, br",
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.ui-flavor": UI_FLAVOR,
}
body = {
"operationName": operation_name,
"variables": variables,
"extensions": {"persistedQuery": QUERY_IDS[operation_name]},
}
membership_header, membership_status_response = msl_client.send_message(
endpoint=GRAPHQL_URL,
params={},
application_data=body,
headers=headers,
)
if isinstance(membership_status_response, dict) and "errors" in membership_status_response:
raise RuntimeError(json.dumps(membership_status_response["errors"], indent=2))
log.info("Loading login page")
response = session.get(LOGIN_URL, timeout=30)
response.raise_for_status()
login_html = response.text
clcs_session_id = extract_clcs_session_id(login_html)
rendition_id = extract_rendition_id(login_html)
log.info("Submitting password screen update")
session_context: Dict[str, Any] = {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
}
session_context.update({
"login.navigationSettings": {"hideOtpToggle": True},
})
full_server_state = {
"realm": "growth",
"name": "PASSWORD_LOGIN",
"clcsSessionId": clcs_session_id,
"sessionContext": session_context,
}
full_screen_update = {
"realm": "custom",
"name": "growthLoginByPassword",
"metadata": {"recaptchaSiteKey": RECAPTCHA_SITE_KEY},
"loggingAction": "Submitted",
"loggingCommand": "SubmitCommand",
"referrerRenditionId": rendition_id,
}
full_variables = {
"format": "HTML",
"imageFormat": "PNG",
"locale": "en-US",
"serverState": json.dumps(full_server_state, separators=(",", ":")),
"serverScreenUpdate": json.dumps(full_screen_update, separators=(",", ":")),
"inputFields": [
{"name": "password", "value": {"stringValue": PASSWORD}},
{"name": "userLoginId", "value": {"stringValue": EMAIL}},
{"name": "countryCode", "value": {"stringValue": "1"}},
{"name": "countryIsoCode", "value": {"stringValue": "US"}},
{"name": "recaptchaResponseTime", "value": {"intValue": 445}},
{"name": "recaptchaResponseToken", "value": {"stringValue": recaptcha_token}},
],
}
try:
operation_name = "CLCSScreenUpdate"
headers = {
"Host": "ios.prod.cloud.netflix.com",
"Connection": "keep-alive",
"X-Netflix.Request.Client.Context": REQUEST_CLIENT_CONTEXT_PASSWORD,
"Content-Encoding": "msl_v1",
"x-netflix.context.feature-capabilities": FEATURE_CAPABILITIES,
"x-netflix.context.operation-name": operation_name,
"X-Netflix.request.expiry.timeout": "15000",
"X-Netflix.Request.Id": generate_request_id(),
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.form-factor": FORM_FACTOR,
"X-Netflix.Request.Attempt": "1",
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept-Language": "en-US,en;q=0.9",
"Accept": "*/*",
"Content-Type": "application/json",
"x-netflix.context.locales": LOCALE,
"x-netflix.context.os-version": OS_VERSION,
"Accept-Encoding": "gzip, deflate, br",
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.ui-flavor": UI_FLAVOR,
}
body = {
"operationName": operation_name,
"variables": full_variables,
"extensions": {
"persistedQuery": QUERY_IDS[operation_name]
},
}
login_header, login_response = msl_client.send_message(endpoint=GRAPHQL_URL,
params={},
application_data=body,
headers=headers)
data = login_response.get("data", {}) if isinstance(login_response, dict) else {}
result = data.get("result", {}) if isinstance(data, dict) else {}
status = result.get("status")
encrypted_header_b64 = login_header.get("headerdata")
header_data = {}
if encrypted_header_b64:
header_data = decrypt_msl_header(encrypted_header_b64, msl_client.keys.encryption, msl_client.keys.sign)
except Exception:
log.exception("Failed to process the login response")
sys.exit(1)
if status == "SUCCESS":
log.info("LOGIN SUCCESSFUL")
try:
save_session_cookies(session, AUTH_COOKIES_PATH, log)
except Exception:
sys.exit(1)
else:
log.error("LOGIN FAILED")
sys.exit(1)
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Optional
from modules.msl.mgk import MSL_MGK, UserAuthentication
from modules.helpers import (
ensure_output_dir,
save_session_cookies,
)
from modules.config import setup_config
from modules.logging import setup_logger
from modules.session import setup_session
config = setup_config()
EMAIL = config["NETFLIX"]["EMAIL"]
PASSWORD = config["NETFLIX"]["PASSWORD"]
def run_mgk(kpekph_path: Optional[str], esnid: str,
new_msl: bool = False, no_verify: bool = False,
proxy: Optional[str] = None):
log = setup_logger('MGK MSL')
OUTPUT_DIR = ensure_output_dir("mgk")
MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache_mgk.json"
AUTH_COOKIES_PATH = OUTPUT_DIR / "netflix_auth_cookies_mgk.json"
DEVICE_TYPE = "NFANDROID2-PRV-NVIDIASHIELDANDROIDTV2019"
# Resolve ESNID: auto-detect file path vs raw string
esnid_path = Path(esnid)
if esnid_path.is_file():
ESN = MSL_MGK.load_esnid_file(esnid_path)
log.info("Loaded ESNID from file: %s", esnid_path)
else:
ESN = esnid
log.info("Using ESNID as raw string")
# Resolve KpeKph: auto-detect file path vs raw string
kpekph_file_path = None
kpekph_raw = None
if kpekph_path:
kpekph_as_path = Path(kpekph_path)
if kpekph_as_path.is_file():
kpekph_file_path = str(kpekph_as_path)
log.info("Loading KpeKph from file: %s", kpekph_as_path)
else:
kpekph_raw = kpekph_path
log.info("Using KpeKph as raw string")
session = setup_session(verify_tls=not no_verify, proxy=proxy)
_proxy = {"http": proxy, "https": proxy} if proxy else None
log.info("Starting MSL MGK handshake with ESN: %s", ESN)
handshake_headers = MSL_MGK.build_request_headers(
request_name="mintCookies",
esn=ESN,
expiry_timeout=12750,
)
msl_client = MSL_MGK.handshake(
session=session,
sender=ESN,
kpekph_path=kpekph_file_path,
kpekph_raw=kpekph_raw,
msl_keys_path=str(MSL_CACHE_PATH),
cookies=None,
headers=handshake_headers,
proxy=_proxy,
new_msl=new_msl,
)
log.info("MGK handshake completed successfully")
user_auth = UserAuthentication.EmailPassword(EMAIL, PASSWORD).__dict__
manifest_endpoint, manifest_params = MSL_MGK.manifest_request_defaults()
manifest_headers = MSL_MGK.build_request_headers(
request_name="licensedManifest",
esn=ESN,
expiry_timeout=12750,
)
log.info("Sending authenticated MSL request with EMAIL_PASSWORD user auth")
try:
header, payload = msl_client.send_message(
endpoint=manifest_endpoint,
params=manifest_params,
application_data={},
userauthdata=user_auth,
headers=manifest_headers,
)
except Exception:
log.exception("MGK authenticated request failed")
sys.exit(1)
try:
auth_cookies = save_session_cookies(session, AUTH_COOKIES_PATH, log)
except Exception:
sys.exit(1)
result = {
"auth_cookies": auth_cookies,
"header": header,
"payload": payload,
}
log.info("MGK login succeeded")
print(json.dumps(result, indent=2, default=str))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+475
View File
@@ -0,0 +1,475 @@
from __future__ import annotations
import json
import random
import re
import sys
import uuid
from pathlib import Path
from typing import Optional
from urllib.parse import quote
from modules.msl.web import MSL_WEB
from modules.helpers import (
ensure_output_dir,
generate_request_id, generate_hex_id,
extract_clcs_session_id, extract_rendition_id,
)
from modules.config import setup_config
from modules.logging import setup_logger
from modules.session import setup_session
config = setup_config()
EMAIL = config["NETFLIX"]["EMAIL"]
PASSWORD = config["NETFLIX"]["PASSWORD"]
def run_web(new_msl: bool = False, no_verify: bool = False,
recaptcha_token: str = '', proxy: Optional[str] = None):
log = setup_logger('BROWSER MSL')
OUTPUT_DIR = ensure_output_dir("browser")
MSL_CACHE_PATH = OUTPUT_DIR / "msl_keys_cache_web.json"
PRELOGIN_COOKIES_PATH = OUTPUT_DIR / "netflix_prelogin_cookies.json"
AUTH_COOKIES_PATH = OUTPUT_DIR / "netflix_auth_cookies.json"
NETFLIX_HOME_URL = "https://www.netflix.com/"
NETFLIX_CANONICAL_URL = "https://netflix.com/"
GRAPHQL_URL = "https://web.prod.cloud.netflix.com/graphql"
LOGIN_URL = "https://www.netflix.com/login"
BROWSE_URL = "https://www.netflix.com/browse"
MSL_ALE_ENDPOINT = "https://www.netflix.com/nq/msl_v1/nrdjs/pbo_tokens/%5E1.0.0/router"
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/146.0.0.0 Safari/537.36"
)
CLIENT_VERSION = "6.135.459.031"
APP_VERSION = "ve300d66c"
HAWKINS_VERSION = "5.16.0"
UI_FLAVOR = "akira"
MSL_ESN = f"NFCDCH-02-{generate_hex_id(32, uppercase=True)}"
REQUEST_CLIENT_CONTEXT = '{"appstate":"foreground"}'
RECAPTCHA_SITE_KEY = "6Lf8hrcUAAAAAIpQAFW2VFjtiYnThOjZOA5xvLyR"
QUERY_IDS = {
"MembershipStatus": {"id": "3f50f3b3-fff8-48c0-bbd3-5fa2cb04b3c1", "version": 102},
"CLCSScreenUpdate": {"id": "1c276cdf-caef-49cf-b38e-384972c2b47e", "version": 102},
"CLCSSendFeedback": {"id": "079b2271-196b-4edd-b65c-e9439b22e305", "version": 102},
"CLCSInterstitialProfileGate": {"id": "b6e10c7d-0e6f-4921-83b5-177995a80d97", "version": 102},
}
recaptcha_token = ""
verify_tls = not no_verify
restore_prelogin_cookies = True
restore_auth_cookies = False
session = setup_session(verify_tls=verify_tls, proxy=proxy)
_proxy = {"http": proxy, "https": proxy} if proxy else None
if restore_auth_cookies:
MSL_WEB.load_cookiejar(session, AUTH_COOKIES_PATH)
elif restore_prelogin_cookies:
MSL_WEB.load_cookiejar(session, PRELOGIN_COOKIES_PATH)
log.info("Bootstrapping anonymous browser session")
response = session.get(NETFLIX_CANONICAL_URL, timeout=30, allow_redirects=True)
response.raise_for_status()
home_response = session.get(NETFLIX_HOME_URL, timeout=30)
home_response.raise_for_status()
log.info("Sending MembershipStatus probe")
operation_name = "MembershipStatus"
variables = {}
referer = NETFLIX_HOME_URL
originating_url = NETFLIX_HOME_URL
headers = {
"Host": "web.prod.cloud.netflix.com",
"Connection": "keep-alive",
"x-netflix.request.id": generate_request_id(),
"x-netflix.context.operation-name": operation_name,
"x-netflix.request.originating.url": originating_url,
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.locales": "en-us",
"x-netflix.context.ui-flavor": UI_FLAVOR,
"x-netflix.request.toplevel.uuid": str(uuid.uuid4()),
"x-netflix.request.attempt": "1",
"x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT,
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": referer,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"x-netflix.request.client.version": CLIENT_VERSION,
"x-netflix.request.client.id": "ui/akiraWeb",
}
body = {
"operationName": operation_name,
"variables": variables,
"extensions": {"persistedQuery": QUERY_IDS[operation_name]},
}
response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30)
response.raise_for_status()
membership_status_response = response.json()
if "errors" in membership_status_response:
raise RuntimeError(json.dumps(membership_status_response["errors"], indent=2))
log.info("Fetching login page and extracting screen context")
response = session.get(LOGIN_URL, timeout=30)
response.raise_for_status()
login_html = response.text
clcs_session_id = extract_clcs_session_id(login_html)
rendition_id = extract_rendition_id(login_html)
screen_name = "IDENTIFICATION"
screen_name = "PASSWORD_LOGIN"
log.info("Submitting password step directly to PASSWORD_LOGIN")
from typing import Any, Dict
session_context: Dict[str, Any] = {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
}
session_context.update({
"login.navigationSettings": {"hideOtpToggle": True},
})
full_server_state = {
"realm": "growth",
"name": "PASSWORD_LOGIN",
"clcsSessionId": clcs_session_id,
"sessionContext": session_context,
}
full_screen_update = {
"realm": "custom",
"name": "growthLoginByPassword",
"metadata": {"recaptchaSiteKey": RECAPTCHA_SITE_KEY},
"loggingAction": "Submitted",
"loggingCommand": "SubmitCommand",
"referrerRenditionId": rendition_id,
}
full_variables = {
"format": "HTML",
"imageFormat": "PNG",
"locale": "en-US",
"serverState": json.dumps(full_server_state, separators=(",", ":")),
"serverScreenUpdate": json.dumps(full_screen_update, separators=(",", ":")),
"inputFields": [
{"name": "password", "value": {"stringValue": PASSWORD}},
{"name": "userLoginId", "value": {"stringValue": EMAIL}},
{"name": "countryCode", "value": {"stringValue": "1"}},
{"name": "countryIsoCode", "value": {"stringValue": "US"}},
{"name": "recaptchaResponseTime", "value": {"intValue": 445}},
{"name": "recaptchaResponseToken", "value": {"stringValue": recaptcha_token}},
],
}
try:
operation_name = "CLCSScreenUpdate"
referer = NETFLIX_HOME_URL
originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(full_server_state, separators=(',', ':')))}"
headers = {
"Host": "web.prod.cloud.netflix.com",
"Connection": "keep-alive",
"x-netflix.request.id": generate_request_id(),
"x-netflix.context.operation-name": operation_name,
"x-netflix.request.originating.url": originating_url,
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.locales": "en-us",
"x-netflix.context.ui-flavor": UI_FLAVOR,
"x-netflix.request.toplevel.uuid": str(uuid.uuid4()),
"x-netflix.request.attempt": "1",
"x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT,
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": referer,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"x-netflix.request.client.version": CLIENT_VERSION,
"x-netflix.request.client.id": "ui/akiraWeb",
}
body = {
"operationName": operation_name,
"variables": full_variables,
"extensions": {"persistedQuery": QUERY_IDS[operation_name]},
}
response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30)
response.raise_for_status()
login_response = response.json()
if "errors" in login_response:
raise RuntimeError(json.dumps(login_response["errors"], indent=2))
except Exception as exc:
log.warning("Full PASSWORD_LOGIN submit failed, retrying with minimal payload: %s", exc)
session_context = {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
}
minimal_server_state = {
"realm": "growth",
"name": "PASSWORD_LOGIN",
"clcsSessionId": clcs_session_id,
"sessionContext": session_context,
}
minimal_screen_update = {
"realm": "custom",
"name": "growthLoginByPassword",
}
minimal_variables = {
"format": "HTML",
"imageFormat": "PNG",
"locale": "en-US",
"serverState": json.dumps(minimal_server_state, separators=(",", ":")),
"serverScreenUpdate": json.dumps(minimal_screen_update, separators=(",", ":")),
"inputFields": [
{"name": "userLoginId", "value": {"stringValue": EMAIL}},
{"name": "password", "value": {"stringValue": PASSWORD}},
],
}
operation_name = "CLCSScreenUpdate"
referer = NETFLIX_HOME_URL
originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(minimal_server_state, separators=(',', ':')))}"
headers = {
"Host": "web.prod.cloud.netflix.com",
"Connection": "keep-alive",
"x-netflix.request.id": generate_request_id(),
"x-netflix.context.operation-name": operation_name,
"x-netflix.request.originating.url": originating_url,
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.locales": "en-us",
"x-netflix.context.ui-flavor": UI_FLAVOR,
"x-netflix.request.toplevel.uuid": str(uuid.uuid4()),
"x-netflix.request.attempt": "1",
"x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT,
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": referer,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"x-netflix.request.client.version": CLIENT_VERSION,
"x-netflix.request.client.id": "ui/akiraWeb",
}
body = {
"operationName": operation_name,
"variables": minimal_variables,
"extensions": {"persistedQuery": QUERY_IDS[operation_name]},
}
response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30)
response.raise_for_status()
login_response = response.json()
if "errors" in login_response:
raise RuntimeError(json.dumps(login_response["errors"], indent=2))
response_text = json.dumps(login_response, ensure_ascii=False)
screen_names = re.findall(r'"name":"([A-Z_]+)"', response_text)
rendition_ids = re.findall(r'"renditionId":"([0-9a-f\-]{36})"', response_text)
clcs_match = re.search(r'"clcsSessionId":"([0-9a-f\-]{36})"', response_text)
next_clcs_session_id = clcs_match.group(1) if clcs_match else clcs_session_id
next_screen_name = screen_names[-1] if screen_names else "PASSWORD_LOGIN"
next_rendition_id = rendition_ids[-1] if rendition_ids else rendition_id
feedback_payload = None
effect = login_response.get("data", {}).get("result", {}).get("effect", {})
nodes = effect.get("nodes", []) if isinstance(effect, dict) else []
for node in nodes:
if node.get("__typename") == "CLCSSendFeedback" and node.get("serverFeedback"):
feedback_payload = json.loads(node["serverFeedback"])
break
if feedback_payload:
log.info("Sending CLCSSendFeedback after successful login")
session_context = {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
}
session_context.update({
"login.navigationSettings": {"hideOtpToggle": True},
})
feedback_server_state = {
"realm": "growth",
"name": "PASSWORD_LOGIN",
"clcsSessionId": next_clcs_session_id,
"sessionContext": session_context,
}
operation_name = "CLCSSendFeedback"
feedback_variables = {
"inputFields": [],
"serverFeedback": json.dumps(feedback_payload, separators=(",", ":")),
"serverState": json.dumps(feedback_server_state, separators=(",", ":")),
}
referer = NETFLIX_HOME_URL
originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(feedback_server_state, separators=(',', ':')))}"
headers = {
"Host": "web.prod.cloud.netflix.com",
"Connection": "keep-alive",
"x-netflix.request.id": generate_request_id(),
"x-netflix.context.operation-name": operation_name,
"x-netflix.request.originating.url": originating_url,
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.locales": "en-us",
"x-netflix.context.ui-flavor": UI_FLAVOR,
"x-netflix.request.toplevel.uuid": str(uuid.uuid4()),
"x-netflix.request.attempt": "1",
"x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT,
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": referer,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"x-netflix.request.client.version": CLIENT_VERSION,
"x-netflix.request.client.id": "ui/akiraWeb",
}
body = {
"operationName": operation_name,
"variables": feedback_variables,
"extensions": {"persistedQuery": QUERY_IDS[operation_name]},
}
response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30)
response.raise_for_status()
feedback_response = response.json()
if "errors" in feedback_response:
raise RuntimeError(json.dumps(feedback_response["errors"], indent=2))
else:
log.info("No post-login feedback payload was found")
log.info("Opening /browse to finalize the authenticated web session")
session_context = {
"session-breadcrumbs": {"funnel_name": "loginWeb"},
}
session_context.update({
"login.navigationSettings": {"hideOtpToggle": True},
})
browse_server_state = {
"realm": "growth",
"name": "PASSWORD_LOGIN",
"clcsSessionId": next_clcs_session_id,
"sessionContext": session_context,
}
browse_originating_url = f"{LOGIN_URL}?serverState={quote(json.dumps(browse_server_state, separators=(',', ':')))}"
response = session.get(
BROWSE_URL,
headers={
"Referer": browse_originating_url,
"Accept-Language": "en-US,en;q=0.9",
},
timeout=30,
)
response.raise_for_status()
log.info("Probing the post-login profile gate")
profile_gate_response = None
try:
operation_name = "CLCSInterstitialProfileGate"
variables = {"format": "HTML", "resolutionMode": "WEB_1X"}
referer = NETFLIX_HOME_URL
originating_url = BROWSE_URL
headers = {
"Host": "web.prod.cloud.netflix.com",
"Connection": "keep-alive",
"x-netflix.request.id": generate_request_id(),
"x-netflix.context.operation-name": operation_name,
"x-netflix.request.originating.url": originating_url,
"x-netflix.context.app-version": APP_VERSION,
"x-netflix.context.hawkins-version": HAWKINS_VERSION,
"x-netflix.context.locales": "en-us",
"x-netflix.context.ui-flavor": UI_FLAVOR,
"x-netflix.request.toplevel.uuid": str(uuid.uuid4()),
"x-netflix.request.attempt": "1",
"x-netflix.request.client.context": REQUEST_CLIENT_CONTEXT,
"x-netflix.request.clcs.bucket": "high",
"User-Agent": USER_AGENT,
"Accept": "application/json",
"Content-Type": "application/json",
"Origin": "https://www.netflix.com",
"Referer": referer,
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"x-netflix.request.client.version": CLIENT_VERSION,
"x-netflix.request.client.id": "ui/akiraWeb",
}
body = {
"operationName": operation_name,
"variables": variables,
"extensions": {"persistedQuery": QUERY_IDS[operation_name]},
}
response = session.post(GRAPHQL_URL, json=body, headers=headers, timeout=30)
response.raise_for_status()
profile_gate_response = response.json()
if "errors" in profile_gate_response:
raise RuntimeError(json.dumps(profile_gate_response["errors"], indent=2))
except Exception as exc:
log.warning("Profile gate probe failed: %s", exc)
log.info("Attempting post-login ALE provision")
ale_response = None
try:
final_cookies = MSL_WEB.cookiejar_to_ordered_dict(session.cookies)
req_id = generate_request_id()
endpoint = (
f"{MSL_ALE_ENDPOINT}?reqAttempt=1&reqName=aleProvision&reqId={req_id}"
f"&clienttype={UI_FLAVOR}&uiversion={APP_VERSION}&browsername=chrome"
f"&browserversion=146.0.0.0&osname=windows&osversion=10.0"
)
headers = MSL_WEB.build_request_headers(
request_name="aleProvision",
user_agent=USER_AGENT,
referer=BROWSE_URL,
esn=MSL_ESN,
extra_headers={
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
},
)
ale_response = MSL_WEB.handshake(
msl_keys_path=MSL_CACHE_PATH,
session=session,
sender=MSL_ESN,
new_msl=False,
cookies=final_cookies,
endpoint=endpoint,
headers=headers,
)
except Exception as exc:
log.error("ALE provision failed: %s", exc)
auth_cookies = MSL_WEB.cookiejar_to_ordered_dict(session.cookies)
AUTH_COOKIES_PATH.write_text(json.dumps(auth_cookies, indent=2), encoding="utf-8")
result = {
"auth_cookies": auth_cookies,
}
#print(json.dumps(result, indent=2))