diff --git a/zefie_wtvp_minisrv/client_emu.js b/zefie_wtvp_minisrv/client_emu.js index 9f8c4307..1f05f5fd 100644 --- a/zefie_wtvp_minisrv/client_emu.js +++ b/zefie_wtvp_minisrv/client_emu.js @@ -10,15 +10,19 @@ const WTVShared = require('./includes/classes/WTVShared.js')['WTVShared']; * using the WTVP protocol with proper authentication and service discovery. */ 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.port = port; this.ssid = ssid; this.url = url; + this.request_type_download = request_type_download; this.outputFile = outputFile; this.maxRedirects = maxRedirects; + this.useEncryption = useEncryption; + this.encryptionEnabled = false; this.redirectCount = 0; this.userIdDetected = false; + this.targetUrlFetched = false; // Prevent multiple target URL fetches this.services = new Map(); // Store service name -> {host, port} mappings this.wtvsec = null; this.wtvshared = new WTVShared(); @@ -34,6 +38,7 @@ class WebTVClientSimulator { console.log(`Target: ${host}:${port}`); console.log(`SSID: ${ssid}`); console.log(`Target URL after auth: ${url}`); + console.log(`Encryption: ${useEncryption ? 'enabled' : 'disabled'}`); if (outputFile) { console.log(`Output file: ${outputFile}`); } @@ -53,7 +58,7 @@ class WebTVClientSimulator { /** * 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) => { // Determine host and port for the service let targetHost = this.host; @@ -69,70 +74,61 @@ class WebTVClientSimulator { const socket = new net.Socket(); this.currentSocket = socket; - let responseData = ''; + let responseData = Buffer.alloc(0); socket.connect(targetPort, targetHost, () => { console.log(`Connected to ${targetHost}:${targetPort}`); - // Build WTVP request - const method = data ? 'POST' : 'GET'; - let request = `${method} ${serviceName}:${path}\r\n`; + let requestData; - // Add required headers - request += `wtv-client-serial-number: ${this.ssid}\r\n`; - request += `wtv-client-bootrom-version: 105\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`; - request += `wtv-request-type: primary\r\n`; - request += `wtv-system-cpuspeed: 166187148\r\n`; - request += `wtv-system-sysconfig: 4163328\r\n`; - request += `wtv-disk-size: 8006\r\n`; - - - // Add challenge response if we have one - if (this.challengeResponse) { - request += `wtv-challenge-response: ${this.challengeResponse}\r\n`; - console.log('Added challenge response to request'); - this.challengeResponse = null; // Clear challenge response after adding to request - } - - // Add ticket if we have one - if (this.ticket) { - request += `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}`; + if (this.encryptionEnabled && this.wtvsec) { + // Send encrypted request + requestData = this.buildEncryptedRequest(serviceName, path, data); } else { - request += '\r\n'; + // Send regular request + requestData = this.buildRegularRequest(serviceName, path, data); } console.log('Sending request:'); - console.log(request); - socket.write(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 += chunk.toString(); - console.log(`Received chunk: ${chunk.toString().length} bytes`); + responseData = Buffer.concat([responseData, chunk]); + console.log(`Received chunk: ${chunk.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); + // 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 && !responseData.includes('\n\n')) { - console.log('Processing incomplete response on close...'); - this.handleResponse(responseData, resolve, reject); + 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); } }); @@ -151,9 +147,174 @@ class WebTVClientSimulator { } /** - * Handle the response from the server + * Build a regular (unencrypted) WTVP request */ - handleResponse(responseData, resolve, reject) { + buildRegularRequest(serviceName, path, data = null) { + const method = data ? 'POST' : 'GET'; + let request = `${method} ${serviceName}:${path}\r\n`; + + // Add required headers + request += `wtv-client-serial-number: ${this.ssid}\r\n`; + request += `wtv-client-bootrom-version: 105\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`; + request += `wtv-request-type: ${((this.request_type_download) ? 'download' : 'primary')}\r\n`; + request += `wtv-system-cpuspeed: 166187148\r\n`; + request += `wtv-system-sysconfig: 4163328\r\n`; + request += `wtv-disk-size: 8006\r\n`; + + // Add challenge response if we have one + if (this.challengeResponse) { + request += `wtv-challenge-response: ${this.challengeResponse}\r\n`; + console.log('Added challenge response to request'); + this.challengeResponse = null; // Clear challenge response after adding to request + } + + // Add ticket if we have one + if (this.ticket) { + request += `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'; + } + + return Buffer.from(request, 'utf8'); + } + + /** + * Build an encrypted WTVP request + */ + 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(responseData); @@ -196,8 +357,9 @@ class WebTVClientSimulator { this.currentSocket = null; } - // Check if user-id was detected (authentication successful) - if (this.userIdDetected) { + // Check if user-id was detected (authentication successful) and target URL not yet fetched + 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} ***`); setTimeout(() => { this.fetchTargetUrl() @@ -207,8 +369,8 @@ class WebTVClientSimulator { return; } - // Follow wtv-visit if present and not authenticated yet - if (headers['wtv-visit']) { + // Follow wtv-visit if present and not authenticated yet, and not skipping redirects + if (headers['wtv-visit'] && !skipRedirects) { if (this.redirectCount >= this.maxRedirects) { console.log(`Maximum redirects (${this.maxRedirects}) reached, stopping`); resolve({ headers, body, status: statusLine, stopped: true }); @@ -223,7 +385,11 @@ class WebTVClientSimulator { .catch(reject); }, 100); // Reduced timeout for faster response } else { - console.log('No wtv-visit header found, resolving...'); + if (skipRedirects && headers['wtv-visit']) { + console.log(`Skipping wtv-visit redirect: ${headers['wtv-visit']}`); + } else { + console.log('No wtv-visit header found, resolving...'); + } resolve({ headers, body, status: statusLine }); } @@ -305,6 +471,14 @@ class WebTVClientSimulator { if (headers['user-id']) { console.log(`*** Authentication successful! user-id detected: ${headers['user-id']} ***`); 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 } } @@ -333,6 +507,18 @@ class WebTVClientSimulator { async fetchTargetUrl() { 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 const match = this.url.match(/^([\w-]+):\/?(.*)/); if (match) { @@ -341,7 +527,7 @@ class WebTVClientSimulator { console.log(`Parsed target service: ${serviceName}, path: ${path}`); 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 if (result.body) { @@ -356,6 +542,10 @@ class WebTVClientSimulator { console.log('No body content received from target URL'); } + console.log('\n*** Request completed successfully ***'); + this.cleanup(); + process.exit(0); + return result; } catch (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 */ @@ -547,7 +628,9 @@ function parseArgs() { ssid: '8100000000000001', url: 'wtv-home:/home', outputFile: null, - maxRedirects: 10 + maxRedirects: 10, + useEncryption: false, + request_type_download: false }; for (let i = 0; i < args.length; i++) { @@ -582,6 +665,12 @@ function parseArgs() { config.outputFile = args[++i]; } break; + case '--download': + config.request_type_download = true; + break; + case '--encryption': + config.useEncryption = true; + break; case '--help': console.log(` WebTV Client Simulator @@ -595,6 +684,8 @@ Options: --url Target URL to fetch after authentication (default: wtv-home:/home) --file Save response body to file instead of echoing to CLI --max-redirects 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 Example: @@ -612,7 +703,7 @@ Example: */ async function main() { 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 process.on('SIGINT', () => {