more client_emu work
This commit is contained in:
@@ -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,16 +224,33 @@ 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) {
|
||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user