Skip to main content

ZileWatch Infrastructure Series -- Part 2

· 21 min read
Stephen Karani
Reverse engineer, Network Engineer

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

  1. Introduction
  2. The Problem: V4 Authentication Broke
  3. Discovery Phase: Finding the New Auth Source
  4. The Breakthrough: Deobfuscated Source Code
  5. Understanding V5 Authentication
  6. Implementation Architecture
  7. Challenges and Solutions
  8. Production Deployment
  9. Lessons Learned
  10. 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:

// V4 Authentication (BROKEN)
const authToken = await fetchJWT("hitsplay.fun"); // JWT-based auth
const nonce = await computeWASMPoW(channel, streamId); // WebAssembly PoW
const key = await fetchKey(keyUrl, {
Authorization: `Bearer ${authToken}`,
"X-Key-Nonce": nonce
// No X-Key-Path or X-Fingerprint headers!
});

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.

# Old (V3, working 2025):
https://topembed.pw/premiumtv/daddyhd.php?id=295

# V4 (broken, Jan 2026):
https://hitsplay.fun/premiumtv/daddyhd.php?id=295

# V5 (NEW, Feb 2026):
https://epaly.fun/premiumtv/daddyhd.php?id=295
# Later changed to: lefttoplay.xyz

Why they keep changing domains: Anti-scraping. By frequently rotating domains, they make it harder for proxy services to keep up.

Step 2: New Token Format Discovered

Fetching from the new domain revealed a completely different authentication structure:

<!-- Old V4: JWT in JavaScript -->
<script>
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
</script>

<!-- New V5: Pipe-delimited in EPlayerAuth -->
<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:

  1. Token is now pipe-delimited: channelKey|country|issuedAt|expiresAt|signature
  2. New field channelSalt - 64 characters of hex
  3. 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:

// Actual deobfuscated code from epaly.fun/obfuscated.js
window.EPlayerAuth = {
init: function (config) {
// Token validation
if (!config.authToken || !config.channelKey || !config.channelSalt) {
throw new Error("Missing required auth config");
}

// Store for later use
_authData.authToken = config.authToken;
_authData.channelKey = config.channelKey;
_authData.channelSalt = config.channelSalt;

return true;
},

getXhrSetup: function () {
return setupXHRInterceptor;
}
};

function setupXHRInterceptor(xhr, url) {
// Check if it's a key request
if (url.match(/\/key\/([^\/]+)\/(\d+)/)) {
const [_, channel, streamId] = url.match(/\/key\/([^\/]+)\/(\d+)/);
const timestamp = Math.floor(Date.now() / 1000);

// Compute PoW nonce
const nonce = computePowNonce(channel, streamId, timestamp);

// Generate fingerprint
const fingerprint = generateFingerprint();

// Compute signature
const signature = computeSignature(channel, streamId, timestamp, fingerprint);

// Set headers
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:

  1. The exact header names and formats
  2. The algorithms for computing PoW, fingerprint, and signature
  3. How channelSalt is used (critical!)
  4. The complete authentication flow

Extracting the Algorithms

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) {
// CRITICAL: Uses channelSalt, NOT a global secret!
const base = HMAC_SHA256(channel, channelSalt);
const target = 0x1000; // Hash must be less than this

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; // Found valid nonce!
}
}

return 99999; // Fallback if no nonce found
}

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

ComponentPurposeSecurity Impact
authTokenProves we fetched from legitimate sourceServer-validated, expires in 24h
channelSaltUnique per-channel secret for crypto opsPrevents cross-channel attacks
PoW NonceRate limiting & anti-spamComputational cost (~1-5 seconds)
FingerprintDevice tracking & anti-sharingBinds to specific browser profile
SignatureRequest integrity & freshnessPrevents tampering & replay

Changes from V3 → V4 → V5

FeatureV3 (2025)V4 (Jan 2026)V5 (Feb 2026)
Auth Sourcetopembed.pwhitsplay.funepaly.fun → lefttoplay.xyz
Auth FormatJWTJWTPipe-delimited string
PoW AlgorithmJavaScript MD5WebAssemblyJavaScript MD5 + channelSalt
FingerprintMD5(UA+res+tz+lang)NoneSHA256(UA+res+tz+lang)
X-Key-PathHMAC with master secretNot requiredHMAC with channelSalt
X-FingerprintRequiredNot requiredRequired
channelSaltNot usedNot usedCritical 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

  1. 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)
  2. 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)

// dlhd-auth-v5.js - Complete V5 implementation
const crypto = require("crypto");
const CryptoJS = require("crypto-js");

/**
* Fetch V5 authentication data from lefttoplay.xyz
*/
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();

// Extract EPlayerAuth.init() call
const initMatch = html.match(/EPlayerAuth\.init\s*\(\s*\{([^}]+)\}\s*\)/);
if (!initMatch) throw new Error("No EPlayerAuth.init() found");

const initBlock = initMatch[1];

// Extract fields using regex
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)
};
}

/**
* Generate V5 fingerprint (SHA256-based)
*/
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);
}

/**
* Compute PoW nonce (V5 algorithm with channelSalt)
*/
function computePoWNonce(channel, streamId, timestamp, channelSalt) {
// Step 1: HMAC-SHA256 base
const base = CryptoJS.HmacSHA256(channel, channelSalt).toString();

// Step 2: Find valid nonce
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;
}
/// Snippet Code ..

Cloudflare Worker Implementation

// dlhd-proxy-v5.ts - Edge computing layer
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; // 4 hours

/**
* Handle key proxy request (V5)
*/
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 });
}

// Extract channel and stream ID
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", "");

// Get cached session or fetch new one
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 });
}

// Compute V5 auth headers
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) + "..."
});

// Call RPI proxy with pre-computed headers
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

// server.js - Residential IP proxy
const http = require("http");

// V5 Key endpoint
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");

// Forward to dvalna.ru with V5 headers
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.

// WRONG ❌
const data = await response.arrayBuffer();
if (data.length === 16) { // ArrayBuffer.length is undefined!

Solution:

// RIGHT ✅
const arrayBuffer = await response.arrayBuffer();
const data = Buffer.from(arrayBuffer);
if (data.length === 16) { // Now .length works correctly

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.

// WRONG ❌
if (data.length === 16 &&
!text.startsWith('{') &&
!text.startsWith('E')) { // Rejects keys starting with byte 0x45 ('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:

// RIGHT ✅
if (data.length === 16 && !text.startsWith("{")) {
// Only reject JSON errors like {"error":"..."}
// Accept all binary data
}

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.

// WRONG ❌
const options = {
redirect: "follow" // This does NOTHING in http.request()!
};
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:

// RIGHT ✅
const proxyReq = http.request(options, proxyRes => {
// Check for redirects
if (proxyRes.statusCode === 301 || proxyRes.statusCode === 302) {
const location = proxyRes.headers.location;
console.log(`Following redirect to: ${location}`);

// Recursively follow the redirect
return proxyRequest(location, res, redirectCount + 1);
}

// Normal response handling
// ...
});

Alternative: Use fetch() (Node.js 18+) which actually follows redirects:

const response = await fetch(targetUrl, {
redirect: "follow" // This WORKS with fetch()
});

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", // ← Add new domain
"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:

// For M3U8/TS requests (segments):
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:

// In Worker's handleSegmentProxy
const session = await fetchV5AuthData(channel, logger, env);

// Pass to RPI with auth parameters
const rpiUrl =
`${rpiBase}/animekai?` +
`url=${encodeURIComponent(segmentUrl)}&` +
`authToken=${encodeURIComponent(session.authToken)}&` +
`channelKey=${session.channelKey}`;
// In RPI's /animekai endpoint
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

# Install Wrangler CLI
npm install -g wrangler

# Login to Cloudflare
wrangler login

# Create new Worker project
wrangler init dlhd-proxy

# Set secrets
wrangler secret put RPI_PROXY_URL
wrangler secret put RPI_PROXY_KEY

# Deploy
wrangler deploy

# Test
curl "https://your-worker.workers.dev/dlhd/auth?channel=295"

RPI Proxy Deployment (PM2)

# Install dependencies
npm install

# Set environment variables
export API_KEY="your-secret-key"
export PORT=3001

# Start with PM2
pm2 start server.js --name proxy-server

# Enable auto-restart
pm2 startup
pm2 save

# Monitor logs
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

// Structured logging with levels
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

  1. Always check for deobfuscated code: The breakthrough came from finding /obfuscated.js wasn't actually obfuscated. Always check for debug/development endpoints.

  2. Type safety matters: TypeScript caught numerous bugs during implementation:

    // Caught at compile time:
    interface V5SessionData {
    channelSalt: string; // Must be present
    }

    // Error: Property 'channelSalt' is missing
    const session: V5SessionData = {
    authToken: "...",
    channelKey: "..."
    };
  3. Binary data requires special handling: Multiple bugs came from treating binary keys as text strings. Always use proper binary types (Buffer, ArrayBuffer, Uint8Array).

  4. 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
  5. Domain rotation is real: In one month, saw three different domains:

    • hitsplay.funepaly.funlefttoplay.xyz

    Solution: Environment variables for domain configuration:

    const AUTH_DOMAIN = env.DLHD_AUTH_DOMAIN || "lefttoplay.xyz";

Security Observations

Why their system works:

  1. Multi-layered defense:

    • Domain rotation (anti-persistence)
    • IP filtering (blocks datacenters)
    • PoW (rate limiting)
    • Fingerprinting (device tracking)
    • HMAC signatures (request integrity)
  2. 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)
  3. Time-based validation: Signatures include timestamps, preventing:

    • Replay attacks
    • Pre-computed request databases
    • Long-term credential theft

Why we succeeded:

  1. Source code exposure: The /obfuscated.js endpoint was the critical vulnerability
  2. Residential IP: RPI proxy bypasses datacenter IP blocks
  3. Exact algorithm replication: Thanks to deobfuscated code, we matched their implementation precisely

Operational Insights

  1. Two-tier architecture is essential:

    • Tier 1 (Cloudflare): Fast, globally distributed, handles crypto
    • Tier 2 (RPI): Residential IP, simple proxy, no business logic
  2. 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

  1. 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.

  2. 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

Token Format

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"

Required Headers by Request Type

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

StatusResponseMeaningSolution
20016 bytes✅ Valid keySuccess
200{"error":"..."}❌ Auth errorRefresh token
401"Unauthorized"Invalid/expired tokenFetch new auth
403"Forbidden"IP blockedUse residential IP
404"Not Found"Invalid channel/streamCheck URL
429"Too Many Requests"Rate limitedImplement backoff
502"Bad Gateway"Server errorRetry with exponential backoff

Performance Benchmarks

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

# Test auth fetch
curl "https://your-worker.workers.dev/dlhd/auth?channel=295"

# Test key fetch
curl "https://your-worker.workers.dev/dlhd/key?url=https%3A%2F%2Fchevy.dvalna.ru%2Fkey%2Fpremium295%2F5893400" | xxd

# Test M3U8
curl "https://your-worker.workers.dev/dlhd?channel=295" | head -30

# Test with MPV
mpv 'https://your-worker.workers.dev/dlhd?channel=295'

# Test RPI directly
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

  1. Discovered new authentication source through manual investigation
  2. Found deobfuscated source code exposing complete algorithms
  3. Reverse-engineered V5 authentication system with 100% accuracy
  4. 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

  1. WebAssembly PoW: Compile PoW to WASM for 10x performance
  2. Distributed caching: Use Cloudflare KV for global session cache
  3. Auto-discovery: Detect domain changes automatically
  4. Metrics dashboard: Real-time monitoring of all endpoints
  5. 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:

  1. Defense in depth matters: Multiple layers (IP filtering, PoW, signatures) make attacks harder
  2. But source code exposure is game over: All the layers become transparent
  3. Operational security is as important as cryptographic security: Proper error handling, logging, and monitoring are essential
  4. 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