Skip to main content

ZileWatch Infrastructure Series -- Part 1

ยท 5 min read
Stephen Karani
Reverse engineer, Network Engineer

Building the API and Aggregation Engines (Technical Deep Dive)โ€‹

ZileWatch is a React Native application powered by a custom-built backend API that acts as the core intelligence layer of the platform.

This document expands into a deeper technical explanation of how the backend aggregation engines are structured, optimized, and scaled.


๐Ÿ— High-Level Architecture

ZileWatch consists of:

  • React Native Client
  • Node.js + Express API Layer
  • Aggregation Engines
  • Caching Layer
  • Provider Adapter Modules

Request Flow:

Client โ†’ API Gateway โ†’ Aggregation Engine โ†’ Provider Adapters โ†’ Normalized Response โ†’ Cache โ†’ Client


๐Ÿง  Core Backend Architecture

1๏ธโƒฃ Modular Folder Structureโ€‹

Example backend structure:

  src/
โ”œโ”€โ”€ controllers/
โ”œโ”€โ”€ routes/
โ”œโ”€โ”€ services/
โ”‚ โ”œโ”€โ”€ movie.engine.ts
โ”‚ โ”œโ”€โ”€ provider.adapter.ts
โ”‚ โ””โ”€โ”€ cache.service.ts
โ”œโ”€โ”€ utils/
โ””โ”€โ”€ middleware/

Each provider is isolated to prevent system-wide failure if one source breaks.


โš™๏ธ Aggregation Engine Internals

Engine Pipelineโ€‹

  1. Validate Request
  2. Normalize Metadata
  3. Check Cache
  4. Execute Parallel Provider Queries
  5. Validate Stream URLs
  6. Normalize Output
  7. Cache Response
  8. Return Response

Provider Abstraction Patternโ€‹

class ProviderAdapter {
async search(title) {}
async getDetails(id) {}
async extractSources(pageUrl) {}
}

Each provider implements the same interface.

This ensures:

  • Pluggable architecture
  • Automatic fallback
  • Horizontal scalability
  • Maintainable codebase

๐Ÿš€ Concurrency & Performance

Parallel Executionโ€‹

Providers are executed using Promise.all():

const results = await Promise.all(providers.map(p => p.extractSources(url)));

This significantly reduces response time compared to sequential execution.


Caching Strategyโ€‹

  • In-memory cache (Redis-ready)
  • TTL-based expiration
  • Hot-content prioritization
  • Cache invalidation hooks

Example:

cache.set(key, data, { ttl: 3600 });

๐Ÿ” Stream Validation Layer

Before returning a stream:

  • HEAD request verification
  • Expiry timestamp validation
  • Content-type confirmation
  • Dead-link filtering

This ensures frontend stability and prevents broken playback.


๐Ÿ›ก Error Handling & Resilience

  • Try/Catch isolation per provider
  • Graceful degradation
  • Structured logging
  • Automatic provider blacklist if repeated failures detected

Actual Implementation

Movies Scraping Engineโ€‹

For the Movies Scraping Engine we use different sources to get the data, we use sources such as goojara, flixer and vidsrc we also extract data from video-hosting-sites such as doodstream and lulustream the Scraping Engine is structured in a way that allows us to easily add new sources and endpoints as needed.

For now we will focus on one source which is goojara, we use goojara to get the video sources for movies, we use Cheerio to scrape the data from goojara and extract the video sources, we also use caching and other optimization techniques to ensure that the data is provided to the API in a way that is fast and efficient.

Docusaurus Plushie

The Above Diagram shows the landing page for the goojara website, this website has been the around for a decade now one the best sites to get Movies and Tv-shows and the best at getting the latest Tv Episodes that are release on the same day, this site uses anti-debug to prevent users from accessing the network tools to prevent scraping,thus we had to use alternative site which acts as probably a mirror for goojara and does not have the same anti-debug techniques, this site is called Levida and it has the same structure as goojara and we can easily scrape it using Cheerio.

We also used a tool that allows the access to the network tools which is executed by script and it is called puppeteer, this tool allows us to access the network tools and scrape the data from goojara without being detected, we use puppeteer to access the network tools and extract the video sources from goojara.

Accessing the network tools allows us to put our detective hat on and observe how the website behaves when we access it, we can see how the website loads the video sources and how it interacts with the user, this allows us to understand how the website works and how we can scrape it without being detected.

Docusaurus Plushie

As we can see from the picture above, we did our investigation by observing on how the website gives the .mp4 source we used a well known Tv-Show Called The Sopranos and we observed that the website gives us the .mp4 source in the network tools, we also observed that the website uses a lot of anti-debug techniques to prevent us from accessing the network tools, but with puppeteer we were able to access the network tools and extract the video sources from goojara without being detected.

Docusaurus Plushie

From the Network Logs we are able to discover interesting things:

  • https://web.wootly.ch/source?id=33c6dd7acd5992a31da149ac2ef52547a19fbc8b&sig=-0aU16aoQJrT3hGlSMDceA&expire=1771102306&ofs=12&usr=141392

This url gives as a response that is the sweet spot it basically shows the video URL with .mp4 format our EndGoal

  • https://turin2.nebula.to/33c6dd7acd5992a31da149ac2ef52547a19fbc8b.mp4?md5=7gvrb5qY668f9PmTZt0GpQ&expires=1771109511&fn=33c6dd7acd5992a31da149ac2ef52547a19fbc8b.mp4

And Wolah!! we have found the actual video source that can be used by any video media player i.e MPV or VLC

๐Ÿ“ˆ Scaling Strategy

Designed for:

  • Docker deployment
  • Horizontal scaling
  • Reverse proxy integration
  • CDN-ready architecture
  • Stateless API nodes

Future improvements:

  • Distributed caching
  • Queue-based provider execution
  • Observability dashboards
  • Auto-scaling clusters

๐Ÿ“Š Response Standardization

All responses follow this format:

{
"title": "Movie Title",
"year": 2026,
"sources": [
{
"quality": "1080p",
"url": "stream-url",
"provider": "provider-name"
}
]
}

Frontend never depends on provider-specific logic.


๐Ÿ”ฎ What's Next?

Part 2 will cover:

  • Live Sports ingestion pipeline
  • HLS handling at scale
  • Real-time channel switching
  • Adaptive bitrate considerations

๐Ÿ“ฒ Download ZileWatch

๐Ÿ‘‰ Android APK:
Download Here

๐Ÿ‘‰ Official Website:
Visit Website


๐Ÿง‘โ€๐Ÿ’ป Author

Engineered by Stephen Zarachii
GitHub: https://github.com/zilezarach

โญ Stay tuned for Part 2.

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

  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

Welcome

ยท One min read
Stephen Karani
Reverse engineer, Network Engineer

This blog will host research write-ups, notes, and experiments related to reverse engineering, malware analysis, and security tooling.