better client emu

This commit is contained in:
zefie
2025-08-09 13:30:15 -04:00
parent 30d0891304
commit 5d53fe2a17

View File

@@ -10,15 +10,19 @@ const WTVShared = require('./includes/classes/WTVShared.js')['WTVShared'];
* using the WTVP protocol with proper authentication and service discovery. * using the WTVP protocol with proper authentication and service discovery.
*/ */
class WebTVClientSimulator { class WebTVClientSimulator {
constructor(host, port, ssid, url, outputFile = null, maxRedirects = 10) { constructor(host, port, ssid, url, outputFile = null, maxRedirects = 10, useEncryption = false, request_type_download = false) {
this.host = host; this.host = host;
this.port = port; this.port = port;
this.ssid = ssid; this.ssid = ssid;
this.url = url; this.url = url;
this.request_type_download = request_type_download;
this.outputFile = outputFile; this.outputFile = outputFile;
this.maxRedirects = maxRedirects; this.maxRedirects = maxRedirects;
this.useEncryption = useEncryption;
this.encryptionEnabled = false;
this.redirectCount = 0; this.redirectCount = 0;
this.userIdDetected = false; this.userIdDetected = false;
this.targetUrlFetched = false; // Prevent multiple target URL fetches
this.services = new Map(); // Store service name -> {host, port} mappings this.services = new Map(); // Store service name -> {host, port} mappings
this.wtvsec = null; this.wtvsec = null;
this.wtvshared = new WTVShared(); this.wtvshared = new WTVShared();
@@ -34,6 +38,7 @@ class WebTVClientSimulator {
console.log(`Target: ${host}:${port}`); console.log(`Target: ${host}:${port}`);
console.log(`SSID: ${ssid}`); console.log(`SSID: ${ssid}`);
console.log(`Target URL after auth: ${url}`); console.log(`Target URL after auth: ${url}`);
console.log(`Encryption: ${useEncryption ? 'enabled' : 'disabled'}`);
if (outputFile) { if (outputFile) {
console.log(`Output file: ${outputFile}`); console.log(`Output file: ${outputFile}`);
} }
@@ -53,7 +58,7 @@ class WebTVClientSimulator {
/** /**
* Make a WTVP request to a service * Make a WTVP request to a service
*/ */
async makeRequest(serviceName, path, data = null, secure = false) { async makeRequest(serviceName, path, data = null, skipRedirects = false) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Determine host and port for the service // Determine host and port for the service
let targetHost = this.host; let targetHost = this.host;
@@ -69,12 +74,82 @@ class WebTVClientSimulator {
const socket = new net.Socket(); const socket = new net.Socket();
this.currentSocket = socket; this.currentSocket = socket;
let responseData = ''; let responseData = Buffer.alloc(0);
socket.connect(targetPort, targetHost, () => { socket.connect(targetPort, targetHost, () => {
console.log(`Connected to ${targetHost}:${targetPort}`); console.log(`Connected to ${targetHost}:${targetPort}`);
// Build WTVP request let requestData;
if (this.encryptionEnabled && this.wtvsec) {
// Send encrypted request
requestData = this.buildEncryptedRequest(serviceName, path, data);
} else {
// Send regular request
requestData = this.buildRegularRequest(serviceName, path, data);
}
console.log('Sending request:');
if (this.encryptionEnabled) {
console.log('[ENCRYPTED REQUEST]');
console.log(`Length: ${requestData.length} bytes`);
} else {
console.log(requestData.toString());
}
socket.write(requestData);
});
socket.on('data', (chunk) => {
responseData = Buffer.concat([responseData, chunk]);
console.log(`Received chunk: ${chunk.length} bytes`);
// Check if we have a complete response
if (this.encryptionEnabled) {
// For encrypted responses, we need to handle differently
this.handleEncryptedResponse(responseData, resolve, reject);
} else {
// Regular response handling
const responseStr = responseData.toString();
if (responseStr.includes('\n\n')) {
console.log('Complete response detected, processing...');
this.handleResponse(responseStr, resolve, reject, skipRedirects);
}
}
});
socket.on('close', () => {
console.log('Connection closed');
if (responseData.length > 0 && !this.encryptionEnabled) {
const responseStr = responseData.toString();
if (!responseStr.includes('\n\n')) {
console.log('Processing incomplete response on close...');
this.handleResponse(responseStr, resolve, reject, skipRedirects);
}
} else if (responseData.length > 0 && this.encryptionEnabled) {
console.log('Processing encrypted response on close...');
this.handleEncryptedResponse(responseData, resolve, reject);
}
});
socket.on('error', (error) => {
console.error('Socket error:', error);
reject(error);
});
// Set timeout
socket.setTimeout(30000, () => {
console.log('Request timed out');
socket.destroy();
reject(new Error('Request timeout'));
});
});
}
/**
* Build a regular (unencrypted) WTVP request
*/
buildRegularRequest(serviceName, path, data = null) {
const method = data ? 'POST' : 'GET'; const method = data ? 'POST' : 'GET';
let request = `${method} ${serviceName}:${path}\r\n`; let request = `${method} ${serviceName}:${path}\r\n`;
@@ -84,12 +159,11 @@ class WebTVClientSimulator {
request += `wtv-client-rom-type: US-LC2-disk-0MB-8MB\r\n`; request += `wtv-client-rom-type: US-LC2-disk-0MB-8MB\r\n`;
request += `wtv-incarnation: ${this.incarnation}\r\n`; request += `wtv-incarnation: ${this.incarnation}\r\n`;
request += `wtv-show-time: 0\r\n`; request += `wtv-show-time: 0\r\n`;
request += `wtv-request-type: primary\r\n`; request += `wtv-request-type: ${((this.request_type_download) ? 'download' : 'primary')}\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`;
// 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`;
@@ -112,48 +186,135 @@ class WebTVClientSimulator {
request += '\r\n'; request += '\r\n';
} }
console.log('Sending request:'); return Buffer.from(request, 'utf8');
console.log(request);
socket.write(request);
});
socket.on('data', (chunk) => {
responseData += chunk.toString();
console.log(`Received chunk: ${chunk.toString().length} bytes`);
// Check if we have a complete response (WTVP uses \n\n for header separation)
if (responseData.includes('\n\n')) {
console.log('Complete response detected, processing...');
this.handleResponse(responseData, resolve, reject);
}
});
socket.on('close', () => {
console.log('Connection closed');
if (responseData && !responseData.includes('\n\n')) {
console.log('Processing incomplete response on close...');
this.handleResponse(responseData, resolve, reject);
}
});
socket.on('error', (error) => {
console.error('Socket error:', error);
reject(error);
});
// Set timeout
socket.setTimeout(30000, () => {
console.log('Request timed out');
socket.destroy();
reject(new Error('Request timeout'));
});
});
} }
/** /**
* Handle the response from the server * Build an encrypted WTVP request
*/ */
handleResponse(responseData, resolve, reject) { buildEncryptedRequest(serviceName, path, data = null) {
// First, check if this is the SECURE ON request
if (serviceName === 'SECURE' && path === 'ON') {
return Buffer.from('SECURE ON\r\n', 'utf8');
}
const method = data ? 'POST' : 'GET';
let request = `${method} ${serviceName}:${path}\r\n`;
// Add headers for encrypted requests
request += `Accept-Language: en-US,en\r\n`;
if (this.ticket) {
request += `wtv-ticket: ${this.ticket}\r\n`;
}
request += `wtv-connect-session-id: ${Math.floor(Math.random() * 0xFFFFFFFF).toString(16)}\r\n`;
request += `wtv-client-serial-number: ${this.ssid}\r\n`;
request += `wtv-system-version: 7181\r\n`;
request += `wtv-capability-flags: 10935ffc8f\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-chipversion: 51511296\r\n`;
request += `User-Agent: Mozilla/4.0 WebTV/2.2.6.1 (compatible; MSIE 4.0)\r\n`;
request += `wtv-encryption: true\r\n`;
request += `wtv-script-id: ${Math.floor(Math.random() * 0x7FFFFFFF) - 0x40000000}\r\n`;
request += `wtv-script-mod: ${Math.floor(Math.random() * 0xFFFFFFFF)}\r\n`;
request += `wtv-incarnation: ${this.incarnation}\r\n`;
if (this.request_type_download) request += 'wtv-request-type: download\r\n';
// Add content if POST
if (data) {
const content = typeof data === 'string' ? data : JSON.stringify(data);
request += `Content-Length: ${content.length}\r\n`;
request += `Content-Type: application/x-www-form-urlencoded\r\n`;
request += `\r\n${content}`;
} else {
request += '\r\n';
}
// Encrypt the request using RC4 with key 0
try {
const requestBuffer = Buffer.from(request, 'utf8');
const encryptedBuffer = this.wtvsec.Encrypt(0, requestBuffer);
return Buffer.from(encryptedBuffer);
} catch (error) {
console.error('Error encrypting request:', error);
return Buffer.from(request, 'utf8');
}
}
/**
* Handle encrypted response data
*/
handleEncryptedResponse(responseData, resolve, reject) {
try {
// Look for the double newline that separates headers from body
const responseStr = responseData.toString('binary');
const headerEndIndex = responseStr.indexOf('\n\n');
if (headerEndIndex === -1) {
// Not a complete response yet
return;
}
// Split headers and body
const headerSection = responseStr.substring(0, headerEndIndex);
const bodyStart = headerEndIndex + 2;
const bodyBuffer = responseData.slice(bodyStart);
console.log('\nReceived encrypted response:');
console.log('Headers:');
console.log(headerSection);
// Parse headers
const lines = headerSection.split('\n');
const statusLine = lines[0].replace('\r', '');
console.log(`Status: ${statusLine}`);
const headers = {};
for (let i = 1; i < lines.length; i++) {
const line = lines[i].replace('\r', '');
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).toLowerCase();
const value = line.substring(colonIndex + 1).trim();
headers[key] = value;
}
}
// Decrypt the body if we have encryption enabled
let body = '';
if (bodyBuffer.length > 0 && headers['wtv-encrypted'] === 'true' && this.wtvsec) {
try {
console.log('Decrypting response body...');
const decryptedBuffer = this.wtvsec.Decrypt(1, bodyBuffer);
body = Buffer.from(decryptedBuffer).toString('utf8');
console.log('Body decrypted successfully');
} catch (error) {
console.error('Error decrypting response body:', error);
body = bodyBuffer.toString('utf8');
}
} else {
body = bodyBuffer.toString('utf8');
}
// Handle special headers
this.processHeaders(headers);
// Close current connection
if (this.currentSocket) {
this.currentSocket.destroy();
this.currentSocket = null;
}
resolve({ headers, body, status: statusLine });
} catch (error) {
console.error('Error processing encrypted response:', error);
reject(error);
}
}
handleResponse(responseData, resolve, reject, skipRedirects = false) {
console.log('\nReceived response:'); console.log('\nReceived response:');
console.log(responseData); console.log(responseData);
@@ -196,8 +357,9 @@ class WebTVClientSimulator {
this.currentSocket = null; this.currentSocket = null;
} }
// Check if user-id was detected (authentication successful) // Check if user-id was detected (authentication successful) and target URL not yet fetched
if (this.userIdDetected) { if (this.userIdDetected && !this.targetUrlFetched) {
this.targetUrlFetched = true; // Set flag to prevent multiple fetches
console.log(`\n*** Authentication complete! Now fetching target URL: ${this.url} ***`); console.log(`\n*** Authentication complete! Now fetching target URL: ${this.url} ***`);
setTimeout(() => { setTimeout(() => {
this.fetchTargetUrl() this.fetchTargetUrl()
@@ -207,8 +369,8 @@ class WebTVClientSimulator {
return; return;
} }
// Follow wtv-visit if present and not authenticated yet // Follow wtv-visit if present and not authenticated yet, and not skipping redirects
if (headers['wtv-visit']) { if (headers['wtv-visit'] && !skipRedirects) {
if (this.redirectCount >= this.maxRedirects) { if (this.redirectCount >= this.maxRedirects) {
console.log(`Maximum redirects (${this.maxRedirects}) reached, stopping`); console.log(`Maximum redirects (${this.maxRedirects}) reached, stopping`);
resolve({ headers, body, status: statusLine, stopped: true }); resolve({ headers, body, status: statusLine, stopped: true });
@@ -222,8 +384,12 @@ class WebTVClientSimulator {
.then(resolve) .then(resolve)
.catch(reject); .catch(reject);
}, 100); // Reduced timeout for faster response }, 100); // Reduced timeout for faster response
} else {
if (skipRedirects && headers['wtv-visit']) {
console.log(`Skipping wtv-visit redirect: ${headers['wtv-visit']}`);
} else { } else {
console.log('No wtv-visit header found, resolving...'); console.log('No wtv-visit header found, resolving...');
}
resolve({ headers, body, status: statusLine }); resolve({ headers, body, status: statusLine });
} }
@@ -305,6 +471,14 @@ class WebTVClientSimulator {
if (headers['user-id']) { if (headers['user-id']) {
console.log(`*** Authentication successful! user-id detected: ${headers['user-id']} ***`); console.log(`*** Authentication successful! user-id detected: ${headers['user-id']} ***`);
this.userIdDetected = true; this.userIdDetected = true;
// Enable encryption if requested and we have WTVSec
if (this.useEncryption && this.wtvsec && !this.encryptionEnabled) {
console.log('*** Enabling encryption after successful authentication ***');
this.wtvsec.SecureOn(); // Initialize RC4 sessions
this.encryptionEnabled = true;
}
return; // Stop processing other headers since we're authenticated return; // Stop processing other headers since we're authenticated
} }
} }
@@ -333,6 +507,18 @@ class WebTVClientSimulator {
async fetchTargetUrl() { async fetchTargetUrl() {
console.log(`Fetching target URL: ${this.url}`); console.log(`Fetching target URL: ${this.url}`);
// If encryption is enabled, send SECURE ON first
if (this.encryptionEnabled) {
console.log('Sending SECURE ON command...');
try {
await this.makeRequest('SECURE ON', '', '', {});
console.log('Encryption successfully enabled');
} catch (error) {
console.error('Failed to enable encryption:', error.message);
throw error;
}
}
// Parse the target URL // Parse the target URL
const match = this.url.match(/^([\w-]+):\/?(.*)/); const match = this.url.match(/^([\w-]+):\/?(.*)/);
if (match) { if (match) {
@@ -341,7 +527,7 @@ class WebTVClientSimulator {
console.log(`Parsed target service: ${serviceName}, path: ${path}`); console.log(`Parsed target service: ${serviceName}, path: ${path}`);
try { try {
const result = await this.makeRequestForContent(serviceName, path); const result = await this.makeRequest(serviceName, path, null, true); // Skip redirects for target URL
// Handle the response // Handle the response
if (result.body) { if (result.body) {
@@ -356,6 +542,10 @@ class WebTVClientSimulator {
console.log('No body content received from target URL'); console.log('No body content received from target URL');
} }
console.log('\n*** Request completed successfully ***');
this.cleanup();
process.exit(0);
return result; return result;
} catch (error) { } catch (error) {
console.error('Error fetching target URL:', error); console.error('Error fetching target URL:', error);
@@ -366,115 +556,6 @@ class WebTVClientSimulator {
} }
} }
/**
* Make a WTVP request specifically for content (doesn't follow redirects)
*/
async makeRequestForContent(serviceName, path, data = null, secure = false) {
return new Promise((resolve, reject) => {
// Determine host and port for the service
let targetHost = this.host;
let targetPort = this.port;
if (this.services.has(serviceName)) {
const service = this.services.get(serviceName);
targetHost = service.host;
targetPort = service.port;
}
console.log(`\n--- Making content request to ${serviceName}:${path} at ${targetHost}:${targetPort} ---`);
const socket = new net.Socket();
this.currentSocket = socket;
let responseData = '';
socket.connect(targetPort, targetHost, () => {
console.log(`Connected to ${targetHost}:${targetPort}`);
// Build WTVP request
const method = data ? 'POST' : 'GET';
let headers = "";
let request = `${method} ${serviceName}:${path}\r\n`;
// Add required headers
headers += `wtv-client-serial-number: ${this.ssid}\r\n`;
headers += `wtv-client-bootrom-version: 2046\r\n`;
headers += `wtv-client-rom-type: US-LC2-disk-0MB-8MB\r\n`;
headers += `wtv-incarnation: ${this.incarnation}\r\n`;
headers += `wtv-show-time: 0\r\n`;
headers += `wtv-request-type: primary\r\n`;
headers += `wtv-system-cpuspeed: 166187148\r\n`;
headers += `wtv-system-sysconfig: 4163328\r\n`;
headers += `wtv-disk-size: 8006\r\n`;
if (secure) headers += `wtv-encryption: true\r\n`;
// Add challenge response if we have one
if (this.challengeResponse) {
headers += `wtv-challenge-response: ${this.challengeResponse}\r\n`;
}
// Add ticket if we have one
if (this.ticket) {
headers += `wtv-ticket: ${this.ticket}\r\n`;
}
// Add content if POST
if (data) {
const content = typeof data === 'string' ? data : JSON.stringify(data);
request += `Content-Length: ${content.length}\r\n`;
request += `Content-Type: application/x-www-form-urlencoded\r\n`;
request += `\r\n${content}`;
} else {
request += '\r\n';
}
console.log('Sending content request:');
if (secure) {
let secure_request = `SECURE ON\r\n`;
secure_request += headers;
secure_request += `\r\n`
this.wtvsec.set_incarnation(this.incarnation);
let sec = this.wtvsec.Encrypt(1, request);
console.log(secure_request + sec);
socket.write(secure_request + sec);
} else {
console.log(request + headers);
socket.write(request + headers);
}
});
socket.on('data', (chunk) => {
responseData += chunk.toString();
console.log(`Received content chunk: ${chunk.toString().length} bytes`);
// Check if we have a complete response
if (responseData.includes('\n\n')) {
console.log('Complete content response detected, processing...');
this.processContentResponse(responseData, resolve, reject);
}
});
socket.on('close', () => {
console.log('Content connection closed');
if (responseData && !responseData.includes('\n\n')) {
console.log('Processing incomplete content response on close...');
this.processContentResponse(responseData, resolve, reject);
}
});
socket.on('error', (error) => {
console.error('Content socket error:', error);
reject(error);
});
// Set timeout
socket.setTimeout(30000, () => {
console.log('Content request timed out');
socket.destroy();
reject(new Error('Content request timeout'));
});
});
}
/** /**
* Process the content response without following redirects * Process the content response without following redirects
*/ */
@@ -547,7 +628,9 @@ function parseArgs() {
ssid: '8100000000000001', ssid: '8100000000000001',
url: 'wtv-home:/home', url: 'wtv-home:/home',
outputFile: null, outputFile: null,
maxRedirects: 10 maxRedirects: 10,
useEncryption: false,
request_type_download: false
}; };
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
@@ -582,6 +665,12 @@ function parseArgs() {
config.outputFile = args[++i]; config.outputFile = args[++i];
} }
break; break;
case '--download':
config.request_type_download = true;
break;
case '--encryption':
config.useEncryption = true;
break;
case '--help': case '--help':
console.log(` console.log(`
WebTV Client Simulator WebTV Client Simulator
@@ -595,6 +684,8 @@ Options:
--url <url> Target URL to fetch after authentication (default: wtv-home:/home) --url <url> Target URL to fetch after authentication (default: wtv-home:/home)
--file <filename> Save response body to file instead of echoing to CLI --file <filename> Save response body to file instead of echoing to CLI
--max-redirects <num> Maximum number of wtv-visit redirects (default: 10) --max-redirects <num> Maximum number of wtv-visit redirects (default: 10)
--download Enable 'wtv-request-type: download' for diskmap testing)
--encryption Enable RC4 encryption after authentication
--help Show this help message --help Show this help message
Example: Example:
@@ -612,7 +703,7 @@ Example:
*/ */
async function main() { async function main() {
const config = parseArgs(); const config = parseArgs();
const simulator = new WebTVClientSimulator(config.host, config.port, config.ssid, config.url, config.outputFile, config.maxRedirects); const simulator = new WebTVClientSimulator(config.host, config.port, config.ssid, config.url, config.outputFile, config.maxRedirects, config.useEncryption, config.request_type_download);
// Handle graceful shutdown // Handle graceful shutdown
process.on('SIGINT', () => { process.on('SIGINT', () => {