Hi to everyove,
this is my first post on VideoHelp, but I have read a lot of threads about DRM and HLS to learn (thanks to the community for all the guides and discussions!). I’m working on Kali Linux in a VM and I’m trying to download a video from guruflix.io, a paid course platform where I own the credentials (i can share it if needed). I want to download the videos because they expire at the end of the subscription, and I also want to understand how everything works technically (I’ve already done a lot of research on AES-128, Cloudflare challenges, and HLS parsing).
A very special thank you to A_n_g_e_l_a for the HellYes program – I tried using it and it’s great work!
And also a big thanks to 2nHxWW6GkN1l916N3ayz8HQoi for Widefrog and all the useful insights shared in the community.
Thank you to everyone who has contributed help in similar threads!
The video is HLS with AES-128 per-segment keys (embedded in the manifest like "key:BASE64==", sequential IVs). The manifest is dynamic (new keys/user_id per session), and the site has strong protections (no right-click; bypassed with extension, but opening F12/Network tab redirects to homepage). I used Burp Suite to capture traffic because the site blocks standard inspectors.
What I’ve tried so far (step by step, to show where I’m stuck):
- Capture manifest/segments: Burp Suite and Network tab filtering “playlist” or “fragment”. Captured m3u8 with ~30-40 segments, base64 keys converted to hex for manual decrypt.
- Browser extensions: Tried every possible one (Video DownloadHelper, m3u8 Sniffer, Stream Detector, CocoCut, HLS Downloader, Blob Video Downloader) – none detects or downloads (likely due to dynamic blobs/JS).
- CLI tools: curl with full custom headers (User-Agent, Referer, Cookie, cf_clearance, X-Turnstile-Token, X-Fingerprint) in loop to download segments – always 0 bytes.
- Advanced downloaders: N_m3u8DL-RE (with local manifest and --custom-header), yt-dlp (with --hls-use-mpegts and local file) –
ffmpeg for final mux (doesn’t decrypt embedded keys).
- hls4encdec (not suitable, hardcoded keys).
- Widevineproxy2 tried, but nothing (it’s not Widevine DRM).
- Browser simulation: Playwright and Selenium to bypass Cloudflare (headless, fetch with credentials include) – Playwright crashes on Python 3.13 (KeyError 'deviceDescriptors', fixed with --no-sandbox but still no download).
Custom scripts: Bash/openssl for manual decrypt (aes-128-cbc with hex key/IV), Python/pycryptodome for automatic manifest parsing, decrypt and merging.
Main bugs/problems:
Constant Cloudflare 307 Temporary Redirect to /cdn?data=... (anti-bot) – curl never receives segments (0 bytes).
cf_clearance/X-Turnstile/session tokens expire in minutes – it cpuld be these the main problem?.
Playwright bug on Python 3.13 – fixed but still no download.
Attachments:
[Attachment 90818 - Click to enlarge]
I’ll attach screenshots of Burp with captured manifest and the headers, if you need something else i'll provide it immediately
Questions:
Do i need to bypass Cloudflare 307 redirect on /cdn endpoint with CLI tools or scripts? And how can i do it if it's necessary?
Any way to make N_m3u8DL-RE / yt-dlp follow the redirect and handle per-segment keys with custom headers?
Any specific tool for HLS AES-128 + Cloudflare protection, or explanation of how the /cdn redirect works?
Thanks a lot to everyone for the help – I really appreciate technical explanations to learn more!
If anything in this post violates forum rules, I apologize in advance.
If you cannot reply directly, please feel free to link to similar threads or point me to relevant discussions.
Thanks again!
Closed Thread
Results 1 to 15 of 15
-
-
yt-dlp: correct way to handle Cloudflare + HLS AES-128
yt-dlp already handles:
- 307 redirects
- HLS AES-128
- Per-segment keys
N_m3u8DL-RE: handling redirects and keys properlyCode:yt-dlp \ --allow-unplayable-formats \ --hls-use-mpegts \ --add-header "User-Agent: Mozilla/5.0" \ --add-header "Referer: https://example.com/" \ --add-header "Origin: https://example.com" \ --cookies cookies.txt \ "https://example.com/path/playlist.m3u8"
Code:N_m3u8DL-RE \ "https://example.com/playlist.m3u8" \ --header "User-Agent: Mozilla/5.0" \ --header "Referer: https://example.com/" \ --header "Origin: https://example.com" \ --auto-select \ --enable-parser \ --no-log
Code:N_m3u8DL-RE \ "playlist.m3u8" \ --header "User-Agent: Mozilla/5.0" \ --header "Referer: https://site.com/" \ --header "Origin: https://site.com" \ --header "Cookie: cf_clearance=XXXX; session=YYYY" \ --enable-parser \ --auto-select
Last edited by sesamap159; 14th Jan 2026 at 09:40.
- 307 redirects
-
To simplify things, here's a simple m3u8 downloader version in Python:
orCode:import subprocess from playwright.sync_api import sync_playwright def is_m3u8(url: str) -> bool: return url.endswith(".m3u8") or ".m3u8?" in url def extract_m3u8_from_network(page): # Intercepte toutes les requêtes réseau m3u8_url = None def handle_request(request): nonlocal m3u8_url if ".m3u8" in request.url: m3u8_url = request.url page.on("request", handle_request) return m3u8_url def main(url_video): with sync_playwright() as p: browser = p.firefox.launch(headless=False) context = browser.new_context() page = context.new_page() # Si l'URL est déjà un .m3u8 → pas besoin de Playwright if is_m3u8(url_video): playlist_url = url_video cookies = [] user_agent = context.user_agent else: # Ouvrir la page vidéo page.goto(url_video) page.wait_for_timeout(5000) # Extraire l'URL du .m3u8 depuis les requêtes réseau playlist_url = extract_m3u8_from_network(page) if not playlist_url: print("Impossible de trouver l'URL .m3u8 dans le trafic réseau.") return # Récupérer cookies Cloudflare cookies = context.cookies() # User-Agent réel user_agent = context.user_agent # Construire Cookie header cookie_header = "; ".join([f"{c['name']}={c['value']}" for c in cookies]) # Construire headers pour N_m3u8DL-RE headers = f"User-Agent: {user_agent}; " if cookie_header: headers += f"Cookie: {cookie_header}; " headers += f"Referer: {url_video}" print("Playlist URL:", playlist_url) print("Headers:", headers) # Lancer N_m3u8DL-RE cmd = [ "N_m3u8DL-RE", playlist_url, "--headers", headers, "--auto-select", "--save-name", "video" ] print("Commande exécutée:", " ".join(cmd)) subprocess.run(cmd) if __name__ == "__main__": url_video = input("URL vidéo ou URL .m3u8 : ").strip() main(url_video)
Code:import subprocess from playwright.sync_api import sync_playwright def is_m3u8(url: str) -> bool: return url.endswith(".m3u8") or ".m3u8?" in url def main(url_video): with sync_playwright() as p: browser = p.firefox.launch(headless=False) context = browser.new_context() page = context.new_page() playlist_url = None turnstile_token = None # Interception des requêtes réseau def on_request(request): nonlocal playlist_url, turnstile_token # Détection du .m3u8 if ".m3u8" in request.url: playlist_url = request.url # Détection du X-Turnstile-Token headers = request.headers if "x-turnstile-token" in headers: turnstile_token = headers["x-turnstile-token"] page.on("request", on_request) # Si l'URL est déjà un .m3u8 if is_m3u8(url_video): playlist_url = url_video cookies = [] user_agent = context.user_agent else: # Ouvrir la page vidéo page.goto(url_video) page.wait_for_timeout(6000) # attendre Cloudflare + player # Récupérer cookies Cloudflare cookies = context.cookies() user_agent = context.user_agent # Vérification if not playlist_url: print("Impossible de trouver l'URL .m3u8 dans le trafic réseau.") return # Construction Cookie header cookie_header = "; ".join([f"{c['name']}={c['value']}" for c in cookies]) # Construction headers pour N_m3u8DL-RE headers = f"User-Agent: {user_agent}; " if cookie_header: headers += f"Cookie: {cookie_header}; " if turnstile_token: headers += f"X-Turnstile-Token: {turnstile_token}; " headers += f"Referer: {url_video}" print("\n=== Infos détectées ===") print("Playlist:", playlist_url) print("Turnstile Token:", turnstile_token) print("Cookies:", cookie_header) print("User-Agent:", user_agent) print("=======================\n") # Lancement de N_m3u8DL-RE cmd = [ "N_m3u8DL-RE", playlist_url, "--headers", headers, "--auto-select", "--save-name", "video" ] print("Commande exécutée:") print(" ".join(cmd)) subprocess.run(cmd) if __name__ == "__main__": url_video = input("URL vidéo ou URL .m3u8 : ").strip() main(url_video)The problem arises with cf_clearance & token over a period of a few minutes.Un seul input : url_video
url_site : https://site.com/watch/123
url_m3u8 : https://cdn.site.com/playlist.m3u8Last edited by sesamap159; 14th Jan 2026 at 10:01.
-
First of all, I want to thank you very much for the quick response !
I've tried both (i deleted --enable-parser because it gives me "Unrecognized command or argument '--enable-parser'.") but i always obtain these kind of error
[Attachment 90819 - Click to enlarge] . Did i make some error in copyingthe url? Is there another url that i have to copy?
-
This is the link of the video in the platform : https://guruflix.io/it/courses/db3ff8b4-f6f2-46c2-8e0d-1ca7bacdefdb/lessons/1/0 and these are some of the link i found in the GET request by filtering for "playlist" and that i'm using as m3u8 url (even if they don't contain the word m3u8) : #EXT-X-KEY:METHOD=AES-128,URI="key:hRW5e/B5VU1vASnj3a5T6Q==",IV=0x0000000000000000000000000 0000000
#EXTINF:8.400,
https://api.guruflix.io/fragment/e0f7bdb7-f4b3-42db-8f1e-9543b1f0e9ae/54ae4eebf27fa29b...f-7c28583bc0e7
#EXT-X-KEY:METHOD=AES-128,URI="key:5pcrwj1Ijk//4eyCGk2jGw==",IV=0x0000000000000000000000000000000 1
#EXTINF:8.333,
https://api.guruflix.io/fragment/e0f7bdb7-f4b3-42db-8f1e-9543b1f0e9ae/54ae4eebf27fa29b...f-7c28583bc0e7 . I'v already decrypted the key with a converter from base64 to Hex but i'm unable to use that keys. I don't know what i'm doing wrong here
-
The path to the video playlist.m3u8:
https://guruflix.io/video....etc
[Attachment 90823 - Click to enlarge]
You must include:- cf_clearance
- session
- X-Turnstile-Token (si requis)
- User-Agent, Referer, Origin
orCode:N_m3u8DL-RE "https://api.guruflix.io/video/.../playlist" \ --header "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" \ --header "Referer: https://guruflix.io/" \ --header "Origin: https://guruflix.io" \ --cookie "cf_clearance=...; session=...; _ga=...; ..." \ --auto-select
Code:import subprocess from playwright.sync_api import sync_playwright def is_m3u8(url: str) -> bool: return ".m3u8" in url def extract_cookies(context): return {c["name"]: c["value"] for c in context.cookies()} def build_header_string(user_agent, cookies, turnstile_token, referer): header_list = [] # User-Agent header_list.append(f"User-Agent: {user_agent}") # Cookies if cookies: cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) header_list.append(f"Cookie: {cookie_str}") # Optional token if turnstile_token: header_list.append(f"X-Turnstile-Token: {turnstile_token}") # Referer header_list.append(f"Referer: {referer}") # Join all headers into a single string return "; ".join(header_list) def main(url_video): with sync_playwright() as p: browser = p.firefox.launch_persistent_context( user_data_dir="profil_cf", headless=False ) page = browser.new_page() playlist_url = None turnstile_token = None def on_request(request): nonlocal playlist_url, turnstile_token url = request.url.lower() headers = request.headers if is_m3u8(url): playlist_url = request.url if "x-turnstile-token" in headers: turnstile_token = headers["x-turnstile-token"] page.on("request", on_request) page.goto(url_video) page.wait_for_load_state("networkidle") cookies = extract_cookies(browser) user_agent = page.evaluate("() => navigator.userAgent") print("\n=== Detected Network Information ===") print("User-Agent:", user_agent) print("Cookies:", cookies) print("Turnstile Token:", turnstile_token) print("Detected Manifest URL:", playlist_url) print("=====================================\n") # Build headers for external tool headers = build_header_string( user_agent=user_agent, cookies=cookies, turnstile_token=turnstile_token, referer=url_video ) # Example of how to call an external tool safely if playlist_url: cmd = [ "N_m3u8DL-RE", playlist_url, "--headers", headers, "--auto-select", "--save-name", "output" ] print("Command that would be executed:") print(" ".join(cmd)) # subprocess.run(cmd) # Not executed for safety input("Analysis complete. Press Enter to exit…") if __name__ == "__main__": url = input("Video page URL: ").strip() main(url)Last edited by sesamap159; 14th Jan 2026 at 12:59.
- cf_clearance
-
I've tried also with this link (i changed "cookie" in another "header" because my version of N_m3u8DL-RE doesn't accept it) with fresh new cookies. I've also add the X-Turnstile-Token, the X-Fingerprint and _ga_203VHK83VS cause i've seen that these parameters will change at every new session. Even with this configuration i can't download the video
[Attachment 90824 - Click to enlarge] . Is it possible that N_m3u8DL-RE can't manage this link?
-
I don't think the playlist URL is: https://api.guruflix.io/video/.../playlist but rather https://guruflix.io/video.../playlist
It's a shame that games don't allow testing since it's a paid site.
Have you tried the Python code?
-
I've tried also with the link https://guruflix.io/video.../playlist with fresh new token taken in minutes (i don't think they could change as quickly) but it always give me "error 404 not found"
[Attachment 90826 - Click to enlarge] i'm sure that i'm pretty close to the solution but i don't know ho to unstuck me: I've also tried the python code but i don't understand how to interprete these errors
[Attachment 90827 - Click to enlarge] . Am i doing something wrong that i cannot catch
?
-
Complete Working Script (Chromium, Non‑Persistent Context)
pip install --upgrade playwright
playwright installCode:import subprocess from playwright.sync_api import sync_playwright def is_m3u8(url: str) -> bool: return ".m3u8" in url def extract_cookies(context): return {c["name"]: c["value"] for c in context.cookies()} def build_header_string(user_agent, cookies, turnstile_token, referer): header_list = [] # User-Agent header_list.append(f"User-Agent: {user_agent}") # Cookies if cookies: cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) header_list.append(f"Cookie: {cookie_str}") # Optional token if turnstile_token: header_list.append(f"X-Turnstile-Token: {turnstile_token}") # Referer header_list.append(f"Referer: {referer}") return "; ".join(header_list) def main(url_video): with sync_playwright() as p: # Chromium instead of Firefox + no persistent context browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() playlist_url = None turnstile_token = None def on_request(request): nonlocal playlist_url, turnstile_token url = request.url.lower() headers = request.headers if is_m3u8(url): playlist_url = request.url if "x-turnstile-token" in headers: turnstile_token = headers["x-turnstile-token"] page.on("request", on_request) page.goto(url_video) page.wait_for_load_state("networkidle") cookies = extract_cookies(context) user_agent = page.evaluate("() => navigator.userAgent") print("\n=== Detected Network Information ===") print("User-Agent:", user_agent) print("Cookies:", cookies) print("Turnstile Token:", turnstile_token) print("Detected Manifest URL:", playlist_url) print("=====================================\n") headers = build_header_string( user_agent=user_agent, cookies=cookies, turnstile_token=turnstile_token, referer=url_video ) if playlist_url: cmd = [ "N_m3u8DL-RE", playlist_url, "--headers", headers, "--auto-select", "--save-name", "output" ] print("Command that would be executed:") print(" ".join(cmd)) # subprocess.run(cmd) # Disabled for safety input("Analysis complete. Press Enter to exit…") if __name__ == "__main__": url = input("Video page URL: ").strip() main(url)
-
Thanks bro, i really appreciate your efforts in helping me. I tried the code but every time I launch it after entering the URL it redirects me to the login page where I enter my credentials and flag the anti-bot square (hosted by cloudflare) but every time I flag it, it automatically unchecks me and doesn't allow me to access the platform. I also tried installing playwright-stealth thinking it was a browser issue but the problem persists
[Attachment 90837 - Click to enlarge]
-
hRW5e/B5VU1vASnj3a5T6Q==: This is the specific part of the identifier (the "path" or "query" depending on the schema). In this context, it represents the unique identifier of the key itself, likely a hash or other form of fingerprint (fingerprint) of the Base64-encoded key. I actually converted it to HEX but I don't understand what you're trying to tell me, could you give me a hand please?
Similar Threads
-
Help in getting HLS AES-128 encrypted video key
By tosay4 in forum Video Streaming DownloadingReplies: 21Last Post: 27th Sep 2025, 13:26 -
Need Help Downloading This AES-128 Encrypted HLS Video
By sp8996 in forum Video Streaming DownloadingReplies: 4Last Post: 10th May 2025, 23:21 -
Need help to download HLS AES-128 encrypted video.
By johnyl0 in forum Video Streaming DownloadingReplies: 40Last Post: 7th Feb 2024, 13:51 -
Need help downloading HLS AES-128 encrytped video.
By radeon in forum Video Streaming DownloadingReplies: 33Last Post: 14th Nov 2022, 08:42 -
Mates, Require Assistance in Downloading HLS AES-128 encrytped video
By stigler in forum Video Streaming DownloadingReplies: 4Last Post: 12th Aug 2021, 05:58



