A journey from broken streams to automated proxy infrastructure
February 2026 - This Finale covers how l was able to extract links for live channels sources, the complete story of discovering, deobfuscating, and implementing a production-ready streaming proxy
Table of Contentsโ
- Introduction
- The Problem: V4 Authentication Broke
- Discovery Phase: Finding the New Auth Source
- The Breakthrough: Deobfuscated Source Code
- Understanding V5 Authentication
- Implementation Architecture
- Challenges and Solutions
- Production Deployment
- Lessons Learned
- Technical Appendix
Introductionโ
In January 2026, My IPTV streaming proxy suddenly stopped working this connects ZileWatch to the Live Channels Side. Channels that had been streaming perfectly for months started returning 502 errors. The authentication system had changedโagain.
This is the story of how I reverse-engineered the new authentication system, discovered the exact algorithms through source code deobfuscation, and built a production-ready proxy infrastructure using Cloudflare Workers and a residential IP proxy.
Tech Stack:
- Cloudflare Workers (edge computing)
- Node.js (residential IP proxy)
- TypeScript (type-safe implementation)
- PM2 (process management)
Key Achievement: Successfully reverse-engineered a complete authentication system with proof-of-work, fingerprinting, and HMAC signaturesโall from a single deobfuscated JavaScript file.
The Problem: V4 Authentication Brokeโ
What We Had (V4 - Working Until January 2026)โ
My V4 implementation worked like this:
const authToken = await fetchJWT("hitsplay.fun");
const nonce = await computeWASMPoW(channel, streamId);
const key = await fetchKey(keyUrl, {
Authorization: `Bearer ${authToken}`,
"X-Key-Nonce": nonce
});
What went wrong:
- January 2026: All key requests started returning errors
- Server responded with:
{"error":"Invalid authentication"}
- Our tokens were valid, PoW nonces computed correctly
- Something fundamental had changed
Initial Investigationโ
We started by comparing working browser sessions vs our proxy:
Browser (working):
GET /key/premium295/5893400
Authorization: Bearer premium295|KE|1771068424|1771154824|signature
X-Key-Timestamp: 1771068424
X-Key-Nonce: 12345
X-Key-Path: e8e551de7a04f726
X-Fingerprint: a1b2c3d4e5f6789
Our Proxy (failing):
GET /key/premium295/5893400
Authorization: Bearer eyJhbGciOiJIUzI1NiIs... # Old JWT
X-Key-Nonce: 12345
# Missing X-Key-Path!
# Missing X-Fingerprint!
Two critical headers were missing, and the auth token format had completely changed from JWT to a pipe-delimited format.
Discovery Phase: Finding the New Auth Sourceโ
Step 1: Domain Changedโ
First discovery: The authentication source had moved.
https://topembed.pw/premiumtv/daddyhd.php?id=295
https://hitsplay.fun/premiumtv/daddyhd.php?id=295
https://epaly.fun/premiumtv/daddyhd.php?id=295
Why they keep changing domains: Anti-scraping. By frequently rotating domains, they make it harder for proxy services to keep up.
Fetching from the new domain revealed a completely different authentication structure:
<script>
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
</script>
<script src="/obfuscated.js"></script>
<script>
EPlayerAuth.init({
authToken: "premium295|KE|1771068424|1771154824|5d3a9242629b6a38...",
channelKey: "premium295",
country: "KE",
timestamp: 1771068424,
validDomain: "epaly.fun",
channelSalt: "b1537ee616dded98b35537b2845e7f473d943c2dd8701c43..."
});
</script>
Key observations:
- Token is now pipe-delimited:
channelKey|country|issuedAt|expiresAt|signature
- New field
channelSalt - 64 characters of hex
- Called via
EPlayerAuth.init() instead of inline JavaScript
Step 3: The Critical Discoveryโ
While inspecting network traffic, we noticed the browser was loading /obfuscated.js. Attempting to fetch it directly:
curl https://epaly.fun/obfuscated.js
Jackpot! The file was obfuscated but l copied the obfuscated code and Claude was able to a complete, deobfuscated source code for the authentication system.
The Breakthrough: Deobfuscated Source Codeโ
What We Foundโ
The /obfuscated.js file contained the entire EPlayerAuth implementation in readable JavaScript:
window.EPlayerAuth = {
init: function (config) {
if (!config.authToken || !config.channelKey || !config.channelSalt) {
throw new Error("Missing required auth config");
}
_authData.authToken = config.authToken;
_authData.channelKey = config.channelKey;
_authData.channelSalt = config.channelSalt;
return true;
},
getXhrSetup: function () {
return setupXHRInterceptor;
}
};
function setupXHRInterceptor(xhr, url) {
if (url.match(/\/key\/([^\/]+)\/(\d+)/)) {
const [_, channel, streamId] = url.match(/\/key\/([^\/]+)\/(\d+)/);
const timestamp = Math.floor(Date.now() / 1000);
const nonce = computePowNonce(channel, streamId, timestamp);
const fingerprint = generateFingerprint();
const signature = computeSignature(channel, streamId, timestamp, fingerprint);
xhr.setRequestHeader("Authorization", "Bearer " + _authData.authToken);
xhr.setRequestHeader("X-Key-Timestamp", timestamp);
xhr.setRequestHeader("X-Key-Nonce", nonce);
xhr.setRequestHeader("X-Key-Path", signature);
xhr.setRequestHeader("X-Fingerprint", fingerprint);
}
}
This gave us:
- The exact header names and formats
- The algorithms for computing PoW, fingerprint, and signature
- How
channelSalt is used (critical!)
- The complete authentication flow
Algorithm 1: Fingerprint (SHA256-based)โ
function generateFingerprint() {
const ua = navigator.userAgent;
const resolution = window.screen.width + "x" + window.screen.height;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const language = navigator.language;
const raw = ua + resolution + timezone + language;
return CryptoJS.SHA256(raw).toString().substring(0, 16);
}
V3 vs V5 Comparison:
- V3: Used MD5 hash
- V5: Uses SHA256 hash (more secure)
- V5: Truncated to 16 characters instead of full hash
Algorithm 2: Proof-of-Work Nonceโ
function computePowNonce(channel, streamId, timestamp) {
const base = HMAC_SHA256(channel, channelSalt);
const target = 0x1000;
for (let i = 0; i < 100000; i++) {
const payload = base + channel + streamId + timestamp + i;
const hash = MD5(payload);
const prefix = parseInt(hash.substring(0, 4), 16);
if (prefix < target) {
return i;
}
}
return 99999;
}
Why this matters:
- V4 used WebAssembly for PoW (we couldn't extract the algorithm)
- V5 uses pure JavaScript (we can replicate it exactly)
- The
channelSalt is unique per channel (not a global secret)
- PoW prevents request spamming and rate limiting evasion
Algorithm 3: X-Key-Path Signatureโ
function computeSignature(channel, streamId, timestamp, fingerprint) {
const data = channel + "|" + streamId + "|" + timestamp + "|" + fingerprint;
return HMAC_SHA256(data, channelSalt).substring(0, 16);
}
Purpose:
- Proves we have the correct
channelSalt
- Prevents replay attacks (includes timestamp)
- Binds authentication to device (includes fingerprint)
Understanding V5 Authenticationโ
Complete Authentication Flowโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ V5 Authentication Flow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1. Fetch Auth Data (Cloudflare Worker)
โ
GET https://lefttoplay.xyz/premiumtv/daddyhd.php?id=295
โ
Extract from HTML:
- authToken: "premium295|KE|1771068424|1771154824|signature"
- channelSalt: "b1537ee616dded98b35537b2845e7f473d943c2dd..."
- channelKey: "premium295"
โ
Cache for 4 hours (token validity period)
2. Request Key (when M3U8 player needs it)
โ
Compute authentication headers:
โโ timestamp = Math.floor(Date.now() / 1000)
โโ nonce = computePoWNonce(channel, streamId, timestamp, channelSalt)
โโ fingerprint = generateFingerprint(userAgent, resolution, tz, lang)
โโ signature = computeSignature(channel, streamId, timestamp, fingerprint)
โ
GET https://chevy.dvalna.ru/key/premium295/5893400
Headers:
- Authorization: Bearer premium295|KE|1771068424|1771154824|sig
- X-Key-Timestamp: 1771068424
- X-Key-Nonce: 12345
- X-Key-Path: e8e551de7a04f726
- X-Fingerprint: a1b2c3d4e5f6789
โ
Response: 16 bytes of AES-128 key (binary data)
3. Decrypt Segments (MPV/ffmpeg)
โ
Uses the 16-byte key to decrypt AES-128 encrypted segments
โ
Playable video stream
Why Each Component Mattersโ
| Component | Purpose | Security Impact |
|---|
| authToken | Proves we fetched from legitimate source | Server-validated, expires in 24h |
| channelSalt | Unique per-channel secret for crypto ops | Prevents cross-channel attacks |
| PoW Nonce | Rate limiting & anti-spam | Computational cost (~1-5 seconds) |
| Fingerprint | Device tracking & anti-sharing | Binds to specific browser profile |
| Signature | Request integrity & freshness | Prevents tampering & replay |
Changes from V3 โ V4 โ V5โ
| Feature | V3 (2025) | V4 (Jan 2026) | V5 (Feb 2026) |
|---|
| Auth Source | topembed.pw | hitsplay.fun | epaly.fun โ lefttoplay.xyz |
| Auth Format | JWT | JWT | Pipe-delimited string |
| PoW Algorithm | JavaScript MD5 | WebAssembly | JavaScript MD5 + channelSalt |
| Fingerprint | MD5(UA+res+tz+lang) | None | SHA256(UA+res+tz+lang) |
| X-Key-Path | HMAC with master secret | Not required | HMAC with channelSalt |
| X-Fingerprint | Required | Not required | Required |
| channelSalt | Not used | Not used | Critical component |
Why V4 broke: The removal of X-Key-Path and X-Fingerprint in V4 was our assumption. In reality, these headers were still required but the algorithms had changed to use channelSalt instead of the old master secret.
Implementation Architectureโ
System Overviewโ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ โ โ โ โ โ
โ Client โโโโโโโโโโถโ Cloudflare Workerโโโโโโโโโโถโ RPI Proxy โ
โ (MPV) โ โ (Edge Compute) โ โ (Node.js) โ
โ โโโโโโโโโโโ โโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ
โ โ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Session Cache โ โ dvalna.ru CDN โ
โ (4-hour TTL) โ โ (Residential) โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Why This Architecture?โ
Challenge: dvalna.ru blocks datacenter IPs (like Cloudflare Workers)
Solution: Two-tier proxy architecture
-
Cloudflare Worker (Tier 1):
- Fast edge computing (low latency globally)
- Handles authentication logic
- Computes PoW, fingerprints, signatures
- Caches auth sessions (4-hour TTL)
- Cannot make direct requests to dvalna.ru (blocked)
-
RPI Proxy (Tier 2):
- Residential IP address (not blocked)
- Simple HTTP proxy, no business logic
- Forwards requests with pre-computed headers
- Handles 302 redirects to other Workers
Node.js Implementation (Core Logic)โ
const crypto = require("crypto");
const CryptoJS = require("crypto-js");
async function fetchV5AuthData(channel) {
const url = `https://lefttoplay.xyz/premiumtv/daddyhd.php?id=${channel}`;
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
Referer: "https://lefttoplay.xyz/"
}
});
const html = await response.text();
const initMatch = html.match(/EPlayerAuth\.init\s*\(\s*\{([^}]+)\}\s*\)/);
if (!initMatch) throw new Error("No EPlayerAuth.init() found");
const initBlock = initMatch[1];
const authToken = initBlock.match(/authToken\s*:\s*['"]([^'"]+)['"]/)[1];
const channelKey = initBlock.match(/channelKey\s*:\s*['"]([^'"]+)['"]/)[1];
const channelSalt = initBlock.match(/channelSalt\s*:\s*['"]([^'"]+)['"]/)[1];
return {
authToken,
channelKey,
channelSalt,
expiresAt: extractExpiryFromToken(authToken)
};
}
function generateFingerprint() {
const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
const resolution = "1920x1080";
const timezone = "America/New_York";
const language = "en-US";
const raw = ua + resolution + timezone + language;
const hash = crypto.createHash("sha256").update(raw).digest("hex");
return hash.substring(0, 16);
}
function computePoWNonce(channel, streamId, timestamp, channelSalt) {
const base = CryptoJS.HmacSHA256(channel, channelSalt).toString();
const target = 0x1000;
for (let i = 0; i < 100000; i++) {
const payload = base + channel + streamId + timestamp + i;
const hash = CryptoJS.MD5(payload).toString();
const prefix = parseInt(hash.substring(0, 4), 16);
if (prefix < target) {
return i;
}
}
return 99999;
}
Cloudflare Worker Implementationโ
import { createLogger } from "./logger";
interface V5SessionData {
authToken: string;
channelKey: string;
channelSalt: string;
fetchedAt: number;
expiresAt: number;
}
const sessionCache = new Map<string, V5SessionData>();
const SESSION_TTL = 4 * 60 * 60 * 1000;
async function handleKeyProxyV5(url: URL, env: Env, logger: any): Promise<Response> {
const keyUrl = url.searchParams.get("url");
if (!keyUrl) {
return new Response("Missing url parameter", { status: 400 });
}
const match = keyUrl.match(/\/key\/([^/]+)\/(\d+)/);
if (!match) {
return new Response("Invalid key URL format", { status: 400 });
}
const [_, channelKey, streamId] = match;
const channel = channelKey.replace("premium", "");
let session = sessionCache.get(channel);
if (!session || Date.now() - session.fetchedAt > SESSION_TTL) {
session = await fetchV5AuthData(channel, env, logger);
if (session) {
sessionCache.set(channel, session);
}
}
if (!session) {
return new Response("Failed to fetch auth data", { status: 502 });
}
const timestamp = Math.floor(Date.now() / 1000);
const nonce = await computePoWNonceV5(channelKey, streamId, timestamp, session.channelSalt);
const fingerprint = await generateFingerprintV5();
const signature = await computeSignatureV5(channelKey, streamId, timestamp, fingerprint, session.channelSalt);
logger.info("V5 key auth computed", {
timestamp,
nonce,
fingerprint: fingerprint.substring(0, 8) + "...",
signature: signature.substring(0, 8) + "..."
});
const rpiUrl = new URL(`${env.RPI_PROXY_URL}/dlhd-key-v5`);
rpiUrl.searchParams.set("url", keyUrl);
rpiUrl.searchParams.set("key", env.RPI_PROXY_KEY);
rpiUrl.searchParams.set("authToken", session.authToken);
rpiUrl.searchParams.set("timestamp", timestamp.toString());
rpiUrl.searchParams.set("nonce", nonce.toString());
rpiUrl.searchParams.set("signature", signature);
rpiUrl.searchParams.set("fingerprint", fingerprint);
const rpiResponse = await fetch(rpiUrl.toString());
if (!rpiResponse.ok) {
return new Response("Key fetch failed", { status: 502 });
}
const keyData = await rpiResponse.arrayBuffer();
if (keyData.byteLength !== 16) {
return new Response("Invalid key size", { status: 502 });
}
return new Response(keyData, {
status: 200,
headers: {
"Content-Type": "application/octet-stream",
"Content-Length": "16",
"Access-Control-Allow-Origin": "*"
}
});
}
RPI Proxy Endpointsโ
const http = require("http");
if (reqUrl.pathname === "/dlhd-key-v5") {
const targetUrl = reqUrl.searchParams.get("url");
const authToken = reqUrl.searchParams.get("authToken");
const timestamp = reqUrl.searchParams.get("timestamp");
const nonce = reqUrl.searchParams.get("nonce");
const signature = reqUrl.searchParams.get("signature");
const fingerprint = reqUrl.searchParams.get("fingerprint");
const headers = {
Authorization: `Bearer ${authToken}`,
"X-Key-Timestamp": timestamp,
"X-Key-Nonce": nonce,
"X-Key-Path": signature,
"X-Fingerprint": fingerprint,
Origin: "https://lefttoplay.xyz",
Referer: "https://lefttoplay.xyz/"
};
const keyRes = await fetch(targetUrl, { headers });
const data = await keyRes.arrayBuffer();
res.writeHead(200, {
"Content-Type": "application/octet-stream",
"Content-Length": 16
});
res.end(Buffer.from(data));
}
Challenges and Solutionsโ
Challenge 1: ArrayBuffer vs Buffer Confusionโ
Problem: Keys were being rejected as "invalid" even though they were exactly 16 bytes.
const data = await response.arrayBuffer();
if (data.length === 16) {
Solution:
const arrayBuffer = await response.arrayBuffer();
const data = Buffer.from(arrayBuffer);
if (data.length === 16) {
Lesson: ArrayBuffer doesn't have a .length propertyโit has .byteLength. Always convert to Buffer first.
Challenge 2: Binary Keys Rejected by Validationโ
Problem: Valid encryption keys were being rejected because they started with certain bytes.
if (data.length === 16 &&
!text.startsWith('{') &&
!text.startsWith('E')) {
Why this failed: Binary encryption keys can start with ANY byte (0x00-0xFF). The check !text.startsWith('E') was rejecting ~0.4% of all valid keys (those starting with ASCII 'E').
Solution:
if (data.length === 16 && !text.startsWith("{")) {
}
Lesson: Never make assumptions about binary data content. Only check for known error formats.
Challenge 3: 302 Redirects Not Being Followedโ
Problem: Segments were returning 302 - 0 bytes instead of video data.
const options = {
redirect: "follow"
};
const req = http.request(options, callback);
Why this failed: The redirect: 'follow' option only works with fetch(), not http.request(). Node.js's http.request() never follows redirects automatically.
Solution:
const proxyReq = http.request(options, proxyRes => {
if (proxyRes.statusCode === 301 || proxyRes.statusCode === 302) {
const location = proxyRes.headers.location;
console.log(`Following redirect to: ${location}`);
return proxyRequest(location, res, redirectCount + 1);
}
});
Alternative: Use fetch() (Node.js 18+) which actually follows redirects:
const response = await fetch(targetUrl, {
redirect: "follow"
});
Lesson: Know your HTTP client! http.request() and fetch() have different behaviors.
Challenge 4: Domain Whitelist Blocking New Domainโ
Problem: RPI proxy was blocking requests to lefttoplay.xyz.
[Security] Blocked animekai proxy to unauthorized domain: https://lefttoplay.xyz/...
Solution:
const ALLOWED_DOMAINS = [
"hitsplay.fun",
"epaly.fun",
"lefttoplay.xyz",
"topembed.pw",
"dvalna.ru"
];
Lesson: When authentication sources change domains, update both the fetch URL AND the security whitelist.
Challenge 5: Encrypted Segments Not Decryptingโ
Problem: Keys were fetching successfully, but segments still wouldn't play in MPV.
Root cause: Segments need authentication headers too, not just key requests.
According to the deobfuscated code:
if (url.includes(".m3u8") || url.includes(".ts") || url.includes("/v/")) {
xhr.setRequestHeader("Authorization", "Bearer " + authToken);
xhr.setRequestHeader("X-Channel-Key", channelKey);
xhr.setRequestHeader("X-User-Agent", navigator.userAgent);
}
Solution: Update segment proxy to add V5 auth headers:
const session = await fetchV5AuthData(channel, logger, env);
const rpiUrl =
`${rpiBase}/animekai?` +
`url=${encodeURIComponent(segmentUrl)}&` +
`authToken=${encodeURIComponent(session.authToken)}&` +
`channelKey=${session.channelKey}`;
if (authToken && url.includes("/v/")) {
headers["Authorization"] = `Bearer ${authToken}`;
headers["X-Channel-Key"] = channelKey;
headers["X-User-Agent"] = userAgent;
}
Lesson: Complete authentication flow requires headers on ALL requests (keys AND segments), not just key fetches.
Production Deploymentโ
Cloudflare Worker Deploymentโ
npm install -g wrangler
wrangler login
wrangler init dlhd-proxy
wrangler secret put RPI_PROXY_URL
wrangler secret put RPI_PROXY_KEY
wrangler deploy
curl "https://your-worker.workers.dev/dlhd/auth?channel=295"
RPI Proxy Deployment (PM2)โ
npm install
export API_KEY="your-secret-key"
export PORT=3001
pm2 start server.js --name proxy-server
pm2 startup
pm2 save
pm2 logs proxy-server
Benefits:
- Reduces auth fetch latency from 1-2s to <10ms
- Handles 1000+ concurrent key requests
- Automatic background refresh before expiry
PoW Computation Performance:
Average PoW computation time:
- V4 (WASM): 50-200ms
- V5 (JavaScript): 1-5 seconds
Mitigation:
- Cache sessions aggressively (4-hour TTL)
- Pre-compute fingerprint (doesn't change per session)
- Consider worker warming for popular channels
Monitoring and Loggingโ
const logger = createLogger(request, "info");
logger.info("V5 key request", {
channel: channelKey,
streamId,
timestamp,
nonce,
computeTime: Date.now() - startTime
});
logger.error("Key fetch failed", {
error: err.message,
stack: err.stack,
channel,
url: keyUrl
});
Metrics tracked:
- Total requests per endpoint
- Success/failure rates
- Average PoW computation time
- Cache hit ratio
- Error types and frequencies
Lessons Learnedโ
Technical Insightsโ
-
Always check for deobfuscated code: The breakthrough came from finding /obfuscated.js wasn't actually obfuscated. Always check for debug/development endpoints.
-
Type safety matters: TypeScript caught numerous bugs during implementation:
interface V5SessionData {
channelSalt: string;
}
const session: V5SessionData = {
authToken: "...",
channelKey: "..."
};
-
Binary data requires special handling: Multiple bugs came from treating binary keys as text strings. Always use proper binary types (Buffer, ArrayBuffer, Uint8Array).
-
HTTP client differences matter:
http.request(): No redirect following, no redirect option
fetch(): Follows redirects by default, modern API
axios: Follows redirects, but adds overhead
-
Domain rotation is real: In one month, saw three different domains:
hitsplay.fun โ epaly.fun โ lefttoplay.xyz
Solution: Environment variables for domain configuration:
const AUTH_DOMAIN = env.DLHD_AUTH_DOMAIN || "lefttoplay.xyz";
Security Observationsโ
Why their system works:
-
Multi-layered defense:
- Domain rotation (anti-persistence)
- IP filtering (blocks datacenters)
- PoW (rate limiting)
- Fingerprinting (device tracking)
- HMAC signatures (request integrity)
-
Per-channel secrets: Using channelSalt instead of global secrets means:
- Compromising one channel doesn't affect others
- Can rotate secrets per-channel if abuse detected
- Makes reverse engineering harder (need salt for each channel)
-
Time-based validation: Signatures include timestamps, preventing:
- Replay attacks
- Pre-computed request databases
- Long-term credential theft
Why we succeeded:
- Source code exposure: The
/obfuscated.js endpoint was the critical vulnerability
- Residential IP: RPI proxy bypasses datacenter IP blocks
- Exact algorithm replication: Thanks to deobfuscated code, we matched their implementation precisely
Operational Insightsโ
-
Two-tier architecture is essential:
- Tier 1 (Cloudflare): Fast, globally distributed, handles crypto
- Tier 2 (RPI): Residential IP, simple proxy, no business logic
-
Caching is critical:
- Without caching: Every request takes 1-5 seconds (PoW computation)
- With caching: Most requests take <100ms
- 4-hour TTL matches token validity period
-
Error handling complexity:
Points of failure:
1. Auth domain change (new domain)
2. Auth format change (HTML parsing)
3. Algorithm change (crypto computation)
4. RPI proxy down (network)
5. dvalna.ru blocking (IP ban)
6. Token expiry (time-based)
7. Invalid PoW (computation error)
Each needs specific error handling and retry logic.
-
Testing is difficult:
- Can't test against production without making real requests
- No official API documentation
- Behavior changes without notice
- Rate limiting makes exhaustive testing impossible
Technical Appendixโ
Complete V5 Algorithm Referenceโ
Format: channelKey|country|issuedAt|expiresAt|signature
Example:
premium295|KE|1771068424|1771154824|5d3a9242629b6a38db3f06980b28b783...
Fields:
- channelKey: "premium" + channel number
- country: 2-letter country code (from user's IP)
- issuedAt: Unix timestamp (seconds)
- expiresAt: Unix timestamp (issuedAt + 86400)
- signature: 64-char hex HMAC-SHA256 of token contents
Fingerprint Algorithm (V5)โ
Input:
- userAgent: Full user agent string
- resolution: "widthxheight" (e.g., "1920x1080")
- timezone: IANA timezone (e.g., "America/New_York")
- language: Language code (e.g., "en-US")
Algorithm:
raw = userAgent + resolution + timezone + language
hash = SHA256(raw)
fingerprint = hash.substring(0, 16)
Example:
Input: "Mozilla/5.0 (...)" + "1920x1080" + "America/New_York" + "en-US"
Hash: "2b94b2c6d378a8339f4c7e1a5d8b9c3f..."
Output: "2b94b2c6d378a833"
PoW Nonce Algorithm (V5)โ
Input:
- channel: Channel key (e.g., "premium295")
- streamId: Stream ID from key URL (e.g., "5893400")
- timestamp: Current Unix timestamp
- channelSalt: 64-char hex from auth data
Algorithm:
base = HMAC-SHA256(channel, channelSalt)
target = 0x1000
for i = 0 to 100000:
payload = base + channel + streamId + timestamp + i
hash = MD5(payload)
prefix = parseInt(hash.substring(0, 4), 16)
if prefix < target:
return i // Valid nonce found
return 99999 // Fallback
Example:
channel = "premium295"
streamId = "5893400"
timestamp = 1771068424
channelSalt = "b1537ee616dded98..."
base = HMAC-SHA256("premium295", "b1537ee6...")
= "a8f3c2d1e9b4..."
i = 0: MD5("a8f3c2d1...premium2955893400177106842400") = "f8a3..." (0xf8a3 > 0x1000) โ
i = 1: MD5("a8f3c2d1...premium2955893400177106842401") = "2a1c..." (0x2a1c > 0x1000) โ
...
i = 12: MD5("a8f3c2d1...premium29558934001771068424012") = "0f3a..." (0x0f3a < 0x1000) โ
Output: 12
Signature Algorithm (V5)โ
Input:
- channel: Channel key (e.g., "premium295")
- streamId: Stream ID (e.g., "5893400")
- timestamp: Unix timestamp
- fingerprint: 16-char fingerprint
- channelSalt: 64-char hex
Algorithm:
data = channel + "|" + streamId + "|" + timestamp + "|" + fingerprint
hmac = HMAC-SHA256(data, channelSalt)
signature = hmac.substring(0, 16)
Example:
data = "premium295|5893400|1771068424|2b94b2c6d378a833"
hmac = HMAC-SHA256(data, "b1537ee6...")
= "e8e551de7a04f726f215400cec915a83b1a5c6b7..."
signature = "e8e551de7a04f726"
Auth Fetch (lefttoplay.xyz/premiumtv/daddyhd.php)โ
GET /premiumtv/daddyhd.php?id=295 HTTP/1.1
Host: lefttoplay.xyz
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Referer: https://lefttoplay.xyz/
Key Fetch (chevy.dvalna.ru/key/...)โ
GET /key/premium295/5893400 HTTP/1.1
Host: chevy.dvalna.ru
Authorization: Bearer premium295|KE|1771068424|1771154824|5d3a92...
X-Key-Timestamp: 1771068424
X-Key-Nonce: 12
X-Key-Path: e8e551de7a04f726
X-Fingerprint: 2b94b2c6d378a833
Origin: https://lefttoplay.xyz
Referer: https://lefttoplay.xyz/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
M3U8 Fetch (dvalna.ru/.../mono.css)โ
GET /ddy6/premium295/mono.css HTTP/1.1
Host: ddy6new.dvalna.ru
Authorization: Bearer premium295|KE|1771068424|1771154824|5d3a92...
X-Channel-Key: premium295
X-User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Origin: https://lefttoplay.xyz
Referer: https://lefttoplay.xyz/
Segment Fetch (dvalna.ru/v/...)โ
GET /v/abc123... HTTP/1.1
Host: chevy.dvalna.ru
Authorization: Bearer premium295|KE|1771068424|1771154824|5d3a92...
X-Channel-Key: premium295
X-User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Referer: https://lefttoplay.xyz/
Error Codes and Meaningsโ
| Status | Response | Meaning | Solution |
|---|
| 200 | 16 bytes | โ
Valid key | Success |
| 200 | {"error":"..."} | โ Auth error | Refresh token |
| 401 | "Unauthorized" | Invalid/expired token | Fetch new auth |
| 403 | "Forbidden" | IP blocked | Use residential IP |
| 404 | "Not Found" | Invalid channel/stream | Check URL |
| 429 | "Too Many Requests" | Rate limited | Implement backoff |
| 502 | "Bad Gateway" | Server error | Retry with exponential backoff |
Operation | Latency | Cache Hit | Notes
-----------------------------|----------|-----------|------------------
Auth fetch (cold) | 1.2s | 0% | HTML parse + regex
Auth fetch (warm) | 12ms | 98% | From cache
PoW computation | 1-5s | N/A | Pure computation
Fingerprint generation | <1ms | N/A | Pure computation
Signature computation | <1ms | N/A | Pure computation
Key fetch (via RPI) | 150ms | N/A | Network + crypto
Complete key request (cold) | 6-8s | N/A | Auth + PoW + fetch
Complete key request (warm) | 1-5s | N/A | Cached auth + PoW
M3U8 fetch | 200ms | N/A | Via RPI proxy
Segment fetch | 50-200ms | N/A | Via RPI proxy
Bottleneck: PoW computation (1-5 seconds)
Mitigation:
- Aggressive caching (4-hour TTL)
- Pre-computation where possible
- Consider WebAssembly for 10x speed improvement
Testing Commandsโ
curl "https://your-worker.workers.dev/dlhd/auth?channel=295"
curl "https://your-worker.workers.dev/dlhd/key?url=https%3A%2F%2Fchevy.dvalna.ru%2Fkey%2Fpremium295%2F5893400" | xxd
curl "https://your-worker.workers.dev/dlhd?channel=295" | head -30
mpv 'https://your-worker.workers.dev/dlhd?channel=295'
curl "http://localhost:3001/dlhd-key-v5?\
url=https://chevy.dvalna.ru/key/premium295/5893400&\
key=YOUR_KEY&\
authToken=premium295|KE|...|...|...&\
timestamp=1771068424&\
nonce=12&\
signature=e8e551de7a04f726&\
fingerprint=2b94b2c6d378a833"
Conclusionโ
What I Accomplishedโ
- โ
Discovered new authentication source through manual investigation
- โ
Found deobfuscated source code exposing complete algorithms
- โ
Reverse-engineered V5 authentication system with 100% accuracy
- โ
Implemented production-ready proxy handling 1000+ concurrent requests
โ
**Deployed globally via Cloudflare Workers** with <100ms latency``
โ
Built resilient two-tier architecture bypassing IP blocks
Key Takeawaysโ
For reverse engineering:
- Always check for debug/development endpoints
- Deobfuscated code is the holy grail
- Type safety (TypeScript) prevents implementation bugs
- Test every assumptionโbinary data behaves differently than text
For system design:
- Two-tier architecture solves IP blocking elegantly
- Edge computing (Workers) + residential proxy = fast + reliable
- Aggressive caching is essential (4-hour session cache)
- Error handling must cover ALL failure modes
For security:
- Multi-layered defense (PoW + fingerprinting + HMAC) is effective
- Per-channel secrets (channelSalt) prevent lateral movement
- Domain rotation is a simple but effective anti-persistence technique
- Source code exposure is the weakest link
Future Improvementsโ
- WebAssembly PoW: Compile PoW to WASM for 10x performance
- Distributed caching: Use Cloudflare KV for global session cache
- Auto-discovery: Detect domain changes automatically
- Metrics dashboard: Real-time monitoring of all endpoints
- A/B testing: Compare different PoW strategies
Final Thoughtsโ
This project demonstrated that even sophisticated authentication systems can be reverse-engineered when we observe the network traffic and able to find the authentication code that we deobfuscated. The key lessons:
- Defense in depth matters: Multiple layers (IP filtering, PoW, signatures) make attacks harder
- But source code exposure is game over: All the layers become transparent
- Operational security is as important as cryptographic security: Proper error handling, logging, and monitoring are essential
- Modern tools (Workers, TypeScript) accelerate development: What would have taken weeks in traditional environments took days
The V5 authentication system is well-designed from a cryptographic perspective. However, leaving deobfuscated source code accessible was the critical mistake that enabled complete reverse engineering.
Project Status: Production (February 2026)
Uptime: 80.7%
Channels Supported: 500+
Daily Requests: 100,000+
**Average Latency:** <100ms (cache hit), 1-5s (cache miss)```
This technical writeup documents a reverse engineering exercise for educational purposes. All discoveries were made through publicly accessible endpoints and client-side JavaScript analysis