more client_emu work

This commit is contained in:
zefie
2025-08-09 22:55:59 -04:00
parent 76ee1df629
commit 58038ab6d2

View File

@@ -30,11 +30,13 @@ class WebTVClientSimulator {
this.wtvsec = null; this.wtvsec = null;
this.wtvshared = new WTVShared(); this.wtvshared = new WTVShared();
this.ticket = null; this.ticket = null;
this.incarnation = 1; this.incarnation = 0; // Start at 0, will be incremented to 1 on first request
this.lastHost = null; // Track last host for incarnation management
this.currentSocket = null; this.currentSocket = null;
this.challengeResponse = null; this.challengeResponse = null;
this.initial_key = null; // Store initial key from wtv-initial-key header this.initial_key = null; // Store initial key from wtv-initial-key header
this.hasSeenEncryptedResponse = false; // Track if we've seen an encrypted response this.hasSeenEncryptedResponse = false; // Track if we've seen an encrypted response
this.previousUrl = null; // Store previous URL for Referer header
this.debug = debug; this.debug = debug;
// Load minisrv config to get the initial shared key // Load minisrv config to get the initial shared key
@@ -112,8 +114,14 @@ class WebTVClientSimulator {
/** /**
* Make a WTVP request to a service * Make a WTVP request to a service
*/ */
async makeRequest(serviceName, path, data = null, skipRedirects = false) { async makeRequest(serviceName, path, data = null, skipRedirects = false, post = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const currentUrl = `${serviceName}:${path}`;
// Increment incarnation for each new request (like real WebTV client)
this.incarnation++;
this.debugLog(`Using incarnation: ${this.incarnation} for ${serviceName}:${path}`);
// Determine host and port for the service // Determine host and port for the service
let targetHost = this.host; let targetHost = this.host;
let targetPort = this.port; let targetPort = this.port;
@@ -175,7 +183,7 @@ class WebTVClientSimulator {
if (idx === -1) idx = responseData.indexOf(lflf); if (idx === -1) idx = responseData.indexOf(lflf);
if (idx !== -1) { if (idx !== -1) {
this.debugLog('Complete response detected, processing...'); this.debugLog('Complete response detected, processing...');
this.handleResponse(responseData, resolve, reject, skipRedirects); this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl);
} }
} }
}); });
@@ -190,7 +198,7 @@ class WebTVClientSimulator {
if (idx === -1) idx = responseData.indexOf(lflf); if (idx === -1) idx = responseData.indexOf(lflf);
if (idx === -1) { if (idx === -1) {
this.debugLog('Processing incomplete response on close...'); this.debugLog('Processing incomplete response on close...');
this.handleResponse(responseData, resolve, reject, skipRedirects); this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl);
} }
} }
}); });
@@ -216,20 +224,37 @@ class WebTVClientSimulator {
const method = data ? 'POST' : 'GET'; const method = data ? 'POST' : 'GET';
let request = `${method} ${serviceName}:${path}\r\n`; let request = `${method} ${serviceName}:${path}\r\n`;
// Add required headers // Add Referer header if we have a previous URL
request += `wtv-client-serial-number: ${this.ssid}\r\n`; if (this.previousUrl) {
request += `wtv-client-bootrom-version: 105\r\n`; request += `Referer: ${this.previousUrl}\r\n`;
request += `wtv-client-rom-type: US-LC2-disk-0MB-8MB\r\n`; }
request += `wtv-incarnation: ${this.incarnation}\r\n`;
request += `wtv-show-time: 0\r\n`; // Add required headers (matching real WebTV client from PCAP)
request += `wtv-request-type: ${((this.request_type_download) ? 'download' : 'primary')}\r\n`; request += `wtv-request-type: ${((this.request_type_download) ? 'download' : 'primary')}\r\n`;
request += `wtv-client-serial-number: ${this.ssid}\r\n`;
request += `wtv-client-bootrom-version: 2046\r\n`;
request += `wtv-client-rom-type: US-LC2-disk-0MB-8MB\r\n`;
request += `wtv-system-cpuspeed: 166187148\r\n`; request += `wtv-system-cpuspeed: 166187148\r\n`;
request += `wtv-system-sysconfig: 4163328\r\n`; request += `wtv-system-sysconfig: 4163328\r\n`;
request += `wtv-disk-size: 8006\r\n`; request += `wtv-disk-size: 8006\r\n`;
request += `Accept-Language: en\r\n`;
request += `wtv-incarnation: ${this.incarnation}\r\n`;
// Generate a random 8 character (4 byte) hex code for wtv-connect-session-id
const connectSessionId = Math.random().toString(16).substr(2, 8).padEnd(8, '0');
request += `wtv-connect-session-id: ${connectSessionId}\r\n`
// Add additional headers that real client sends (from PCAP analysis)
request += `User-Agent: Mozilla/4.0 WebTV/2.2.6.1 (compatible; MSIE 4.0)\r\n`;
request += `wtv-system-version: 7181\r\n`;
request += `wtv-capability-flags: 10935ffc8f\r\n`;
request += `wtv-system-chipversion: 51511296\r\n`;
if (this.useEncryption) request += `wtv-encryption: true\r\n`;
if (!this.challengeResponse) request += `wtv-script-id: -1896417432\r\n`;
if (!this.challengeResponse) request += `wtv-script-mod: 1754789923\r\n`;
request += `wtv-client-address: 0.0.0.0\r\n`;
// Add challenge response if we have one // Add challenge response if we have one
if (this.challengeResponse) { if (this.challengeResponse) {
request += `wtv-challenge-response: ${this.challengeResponse}\r\n`; request += `wtv-challenge-response: ${this.challengeResponse}\r\n`;
this.debugLog('Added challenge response to request'); this.debugLog('Added challenge response to request');
this.challengeResponse = null; // Clear challenge response after adding to request this.challengeResponse = null; // Clear challenge response after adding to request
} }
@@ -404,9 +429,15 @@ class WebTVClientSimulator {
reject(error); reject(error);
} }
} }
handleResponse(responseData, resolve, reject, skipRedirects = false) { handleResponse(responseData, resolve, reject, skipRedirects = false, currentUrl = null) {
this.debugLog('\nReceived response:'); this.debugLog('\nReceived response:');
this.debugLog(responseData); this.debugLog(responseData);
// Update previousUrl for next request's Referer header
if (currentUrl) {
this.previousUrl = currentUrl;
}
try { try {
// Find header/body split using CRLF CRLF (\r\n\r\n) or fallback to LF LF (\n\n) // Find header/body split using CRLF CRLF (\r\n\r\n) or fallback to LF LF (\n\n)
let idx = -1; let idx = -1;
@@ -451,10 +482,11 @@ class WebTVClientSimulator {
} }
} }
this.processHeaders(headers); this.processHeaders(headers);
this.debugLog("srv headers:", headers);
// Decompress the body if needed // Decompress the body if needed
bodyBuf = this.decompressBody(bodyBuf, headers); bodyBuf = this.decompressBody(bodyBuf, headers);
this.debugLog("srv body:", bodyBuf.toString());
// Mark that we've seen an encrypted response // Mark that we've seen an encrypted response
if (headers['wtv-encrypted'] === 'true') { if (headers['wtv-encrypted'] === 'true') {
this.hasSeenEncryptedResponse = true; this.hasSeenEncryptedResponse = true;
@@ -487,6 +519,13 @@ class WebTVClientSimulator {
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
}, 100); }, 100);
} else if (headers['wtv-phone-log-url'] && headers['wtv-phone-log-url'].includes("post")) {
this.debugLog(`Following wtv-phone-log-url: ${headers['wtv-phone-log-url']}`);
setTimeout(() => {
this.followVisit(headers['wtv-phone-log-url'], true)
.then(resolve)
.catch(reject);
}, 100);
} else { } else {
if (skipRedirects && headers['wtv-visit']) { if (skipRedirects && headers['wtv-visit']) {
this.debugLog(`Skipping wtv-visit redirect: ${headers['wtv-visit']}`); this.debugLog(`Skipping wtv-visit redirect: ${headers['wtv-visit']}`);
@@ -540,26 +579,49 @@ class WebTVClientSimulator {
// Handle wtv-challenge // Handle wtv-challenge
if (headers['wtv-challenge']) { if (headers['wtv-challenge']) {
this.debugLog('Received wtv-challenge, processing...'); this.debugLog('Received wtv-challenge, processing...');
this.debugLog(`Challenge: ${headers['wtv-challenge']}`);
this.debugLog(`Initial key from server: ${this.initial_key}`);
if (!this.wtvsec) { if (!this.wtvsec) {
this.debugLog('No WTVSec instance, initializing with default key...'); this.debugLog('No WTVSec instance, initializing...');
this.wtvsec = new WTVSec(this.minisrv_config, this.incarnation); this.wtvsec = new WTVSec(this.minisrv_config, this.incarnation);
// Override the initial shared key with the one provided by the server
if (this.initial_key) {
this.debugLog('Overriding WTVSec initial shared key with server-provided key');
this.wtvsec.initial_shared_key = CryptoJS.enc.Base64.parse(this.initial_key);
this.wtvsec.current_shared_key = this.wtvsec.initial_shared_key;
}
} }
try { try {
this.wtvsec.IssueChallenge(); // Ensure WTVSec has the correct incarnation
this.wtvsec.set_incarnation(headers["wtv-incarnation"]); this.wtvsec.set_incarnation(this.incarnation);
const challengeResponse = this.wtvsec.ProcessChallenge(headers['wtv-challenge'], CryptoJS.enc.Base64.parse(this.initial_key));
// Set incarnation from server if provided
if (headers["wtv-incarnation"]) {
this.debugLog(`Setting incarnation from server: ${headers["wtv-incarnation"]}`);
this.wtvsec.set_incarnation(parseInt(headers["wtv-incarnation"]));
this.incarnation = parseInt(headers["wtv-incarnation"]);
}
// Use the server's initial key for challenge processing
const keyToUse = this.initial_key ? CryptoJS.enc.Base64.parse(this.initial_key) : this.wtvsec.current_shared_key;
this.debugLog(`Using key for challenge: ${keyToUse.toString(CryptoJS.enc.Base64)}`);
const challengeResponse = this.wtvsec.ProcessChallenge(headers['wtv-challenge'], keyToUse);
if (challengeResponse && challengeResponse.toString(CryptoJS.enc.Base64)) { if (challengeResponse && challengeResponse.toString(CryptoJS.enc.Base64)) {
this.debugLog('Challenge processed successfully, preparing response'); this.debugLog('Challenge processed successfully, preparing response');
this.debugLog(`Challenge response: ${challengeResponse.toString(CryptoJS.enc.Base64)}`);
// We'll send the challenge response in the next request // We'll send the challenge response in the next request
this.challengeResponse = challengeResponse.toString(CryptoJS.enc.Base64); this.challengeResponse = challengeResponse.toString(CryptoJS.enc.Base64);
//this.incarnation = this.wtvsec.incarnation;
this.debugLog('Setting wtv-challenge-response header for next request'); this.debugLog('Setting wtv-challenge-response header for next request');
} else { } else {
console.error('Failed to process challenge - no response generated'); console.error('Failed to process challenge - no response generated');
} }
} catch (error) { } catch (error) {
console.error('Error processing challenge:', error.message); console.error('Error processing challenge:', error.message);
console.error('Stack trace:', error.stack);
} }
} }
@@ -581,13 +643,7 @@ class WebTVClientSimulator {
// Initialize with current incarnation (which was incremented when we got wtv-encrypted: true) // Initialize with current incarnation (which was incremented when we got wtv-encrypted: true)
this.wtvsec = new WTVSec(this.minisrv_config, this.incarnation); this.wtvsec = new WTVSec(this.minisrv_config, this.incarnation);
} }
// Follow the same sequence as the server to ensure matching keys
if (this.ticket) {
this.wtvsec.DecodeTicket(this.ticket);
this.wtvsec.ticket_b64 = this.ticket;
// Set the incarnation to match current state
this.wtvsec.set_incarnation(this.incarnation);
}
this.wtvsec.SecureOn(); // Initialize RC4 sessions this.wtvsec.SecureOn(); // Initialize RC4 sessions
this.encryptionEnabled = true; this.encryptionEnabled = true;
@@ -599,7 +655,7 @@ class WebTVClientSimulator {
/** /**
* Follow a wtv-visit directive * Follow a wtv-visit directive
*/ */
async followVisit(visitUrl) { async followVisit(visitUrl, post = false) {
this.debugLog(`Parsing wtv-visit URL: ${visitUrl}`); this.debugLog(`Parsing wtv-visit URL: ${visitUrl}`);
// Parse the visit URL: service:/path or service:path // Parse the visit URL: service:/path or service:path
@@ -608,7 +664,7 @@ class WebTVClientSimulator {
const serviceName = match[1]; const serviceName = match[1];
const path = '/' + (match[2] || ''); const path = '/' + (match[2] || '');
this.debugLog(`Parsed service: ${serviceName}, path: ${path}`); this.debugLog(`Parsed service: ${serviceName}, path: ${path}`);
return await this.makeRequest(serviceName, path); return await this.makeRequest(serviceName, path, (post) ? '1' : null, null);
} else { } else {
throw new Error(`Invalid wtv-visit URL: ${visitUrl}`); throw new Error(`Invalid wtv-visit URL: ${visitUrl}`);
} }