diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index 504325dc..fdbc3e21 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -1938,10 +1938,10 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } if (!headers.request_url) { var header_length = 0; - if (data_hex.indexOf("0d0a0d0a")) { + if (data_hex.includes("0d0a0d0a")) { // \r\n\r\n header_length = data.length + 4; - } else if (data_hex.indexOf("0a0a")) { + } else if (data_hex.includes("0a0a")) { // \n\n header_length = data.length + 2; } diff --git a/zefie_wtvp_minisrv/client_emu.js b/zefie_wtvp_minisrv/client_emu.js index 24584e1c..f5a7850e 100644 --- a/zefie_wtvp_minisrv/client_emu.js +++ b/zefie_wtvp_minisrv/client_emu.js @@ -1,7 +1,10 @@ const net = require('net'); const CryptoJS = require('crypto-js'); const WTVSec = require('./includes/classes/WTVSec.js'); +const e = require('express'); const WTVShared = require('./includes/classes/WTVShared.js')['WTVShared']; +const LZPF = require('./includes/classes/LZPF.js'); +const zlib = require('zlib'); /** * WebTV Client Simulator @@ -31,6 +34,7 @@ class WebTVClientSimulator { this.currentSocket = null; this.challengeResponse = null; this.initial_key = null; // Store initial key from wtv-initial-key header + this.hasSeenEncryptedResponse = false; // Track if we've seen an encrypted response this.debug = debug; // Load minisrv config to get the initial shared key @@ -51,6 +55,49 @@ class WebTVClientSimulator { } } + /** + * Decompress response body based on content encoding headers + */ + decompressBody(body, headers) { + if (!Buffer.isBuffer(body) || body.length === 0) { + return body; + } + + try { + // Check for LZPF compression first (WebTV specific) + if (headers['wtv-lzpf'] === '0') { + this.debugLog('Decompressing LZPF compressed body...'); + const lzpf = new LZPF(); + const decompressed = lzpf.expand(body); + this.debugLog(`LZPF decompression: ${body.length} bytes -> ${decompressed.length} bytes`); + return decompressed; + } + + // Check for standard gzip/deflate compression + if (headers['content-encoding']) { + const encoding = headers['content-encoding'].toLowerCase(); + this.debugLog(`Decompressing ${encoding} compressed body...`); + + if (encoding === 'deflate') { + const decompressed = zlib.inflateSync(body); + this.debugLog(`Deflate decompression: ${body.length} bytes -> ${decompressed.length} bytes`); + return decompressed; + } else if (encoding === 'gzip') { + const decompressed = zlib.gunzipSync(body); + this.debugLog(`Gzip decompression: ${body.length} bytes -> ${decompressed.length} bytes`); + return decompressed; + } + } + + // No compression detected, return original body + return body; + } catch (error) { + console.error('Error decompressing response body:', error); + this.debugLog('Returning original compressed body due to decompression error'); + return body; + } + } + /** * Start the simulation by connecting to wtv-1800:/preregister */ @@ -89,22 +136,27 @@ class WebTVClientSimulator { let requestData; if (this.encryptionEnabled && this.wtvsec) { - // Send encrypted request - requestData = this.buildEncryptedRequest(serviceName, path, data); + // For encrypted requests, first send SECURE ON, then immediately send the encrypted request + // This matches the real WebTV client behavior seen in packet captures + this.debugLog('Sending SECURE ON request...'); + const secureOnBuffer = this.buildSecureOnRequest(); + console.log(secureOnBuffer.toString('hex')); + socket.write(secureOnBuffer); + + // Send encrypted request immediately after (as seen in pcap analysis) + setImmediate(() => { + this.debugLog('Sending encrypted request...'); + const encryptedRequestData = this.buildEncryptedRequest(serviceName, path, data); + console.log(encryptedRequestData.toString('hex')); + socket.write(encryptedRequestData); + }); } else { // Send regular request requestData = this.buildRegularRequest(serviceName, path, data); - } - - this.debugLog('Sending request:'); - if (this.encryptionEnabled) { - this.debugLog('[ENCRYPTED REQUEST]'); - this.debugLog(`Length: ${requestData.length} bytes`); - } else { + this.debugLog('Sending request:'); this.debugLog(requestData.toString()); + socket.write(requestData); } - - socket.write(requestData); }); socket.on('data', (chunk) => { @@ -206,23 +258,20 @@ class WebTVClientSimulator { } /** - * Build an encrypted WTVP request + * Build a SECURE ON request (sent in plaintext to establish encryption) */ - 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'); - } + buildSecureOnRequest() { + // Increment incarnation for encrypted session + this.incarnation++; + this.debugLog(`Using incarnation: ${this.incarnation}`); - const method = data ? 'POST' : 'GET'; - let request = `${method} ${serviceName}:${path}\r\n`; - - // Add headers for encrypted requests + // SECURE ON should match real WebTV client exactly - no URL, just the method + let request = `SECURE ON\r\n`; 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-connect-session-id: ${Math.random().toString(16).substr(2, 8)}\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`; @@ -231,9 +280,23 @@ class WebTVClientSimulator { 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`; + request += `wtv-script-id: -154276969\r\n`; + request += `wtv-script-mod: ${Math.floor(Date.now() / 1000)}\r\n`; + request += `wtv-incarnation:${this.incarnation}\r\n`; // Note: no space after colon + request += '\r\n'; + + return Buffer.from(request, 'utf8'); + } + + /** + * Build an encrypted WTVP request + */ + buildEncryptedRequest(serviceName, path, data = null) { + const method = data ? 'POST' : 'GET'; + let request = `${method} ${serviceName}:${path}\r\n`; + + // For encrypted requests, only include the minimal necessary headers + // The SECURE ON already sent the auth and session info if (this.request_type_download) request += 'wtv-request-type: download\r\n'; @@ -247,10 +310,10 @@ class WebTVClientSimulator { request += '\r\n'; } - // Encrypt the request using RC4 with key 0 + // Encrypt the request using RC4 with key 0 (server expects Decrypt(0, enc_data)) try { - const requestBuffer = Buffer.from(request, 'utf8'); - const encryptedBuffer = this.wtvsec.Encrypt(0, requestBuffer); + this.wtvsec.set_incarnation(this.incarnation); // Ensure WTVSec has the correct incarnation + const encryptedBuffer = this.wtvsec.Encrypt(0, request); return Buffer.from(encryptedBuffer); } catch (error) { console.error('Error encrypting request:', error); @@ -263,26 +326,34 @@ class WebTVClientSimulator { */ 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'); + // Find header/body split using CRLF CRLF (\r\n\r\n) or fallback to LF LF (\n\n) + let idx = -1; + let sepLen = 0; + const crlfcrlf = Buffer.from('\r\n\r\n'); + const lflf = Buffer.from('\n\n'); + idx = responseData.indexOf(crlfcrlf); + if (idx !== -1) { + sepLen = 4; + } else { + idx = responseData.indexOf(lflf); + if (idx !== -1) sepLen = 2; + } - if (headerEndIndex === -1) { + if (idx === -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); + // Split headers and body - headers are always plaintext + const headerSection = responseData.slice(0, idx).toString('utf8'); + const bodyBuffer = responseData.slice(idx + sepLen); this.debugLog('\nReceived encrypted response:'); this.debugLog('Headers:'); this.debugLog(headerSection); // Parse headers - const lines = headerSection.split('\n'); + const lines = headerSection.split(/\r?\n/); const statusLine = lines[0].replace('\r', ''); this.debugLog(`Status: ${statusLine}`); @@ -298,7 +369,7 @@ class WebTVClientSimulator { } } - // Decrypt the body if we have encryption enabled + // Decrypt the body if we have encryption enabled and encrypted content let body = Buffer.alloc(0); if (bodyBuffer.length > 0 && headers['wtv-encrypted'] === 'true' && this.wtvsec) { try { @@ -314,9 +385,17 @@ class WebTVClientSimulator { body = bodyBuffer; } + // Decompress the body if needed + body = this.decompressBody(body, headers); + // Handle special headers this.processHeaders(headers); + // Mark that we've seen an encrypted response + if (headers['wtv-encrypted'] === 'true') { + this.hasSeenEncryptedResponse = true; + } + // Close current connection if (this.currentSocket) { this.currentSocket.destroy(); @@ -377,6 +456,15 @@ class WebTVClientSimulator { } } this.processHeaders(headers); + + // Decompress the body if needed + bodyBuf = this.decompressBody(bodyBuf, headers); + + // Mark that we've seen an encrypted response + if (headers['wtv-encrypted'] === 'true') { + this.hasSeenEncryptedResponse = true; + } + if (this.currentSocket) { this.currentSocket.destroy(); this.currentSocket = null; @@ -492,12 +580,23 @@ class WebTVClientSimulator { this.userIdDetected = true; // Enable encryption if requested and we have WTVSec - if (this.useEncryption && this.wtvsec && !this.encryptionEnabled) { + if (this.useEncryption) { this.debugLog('*** Enabling encryption after successful authentication ***'); + if (!this.wtvsec) { + // Initialize with current incarnation (which was incremented when we got wtv-encrypted: true) + 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.encryptionEnabled = true; - } - + } return; // Stop processing other headers since we're authenticated } } @@ -526,18 +625,6 @@ class WebTVClientSimulator { async fetchTargetUrl() { console.log(`Fetching target URL: ${this.url}`); - // If encryption is enabled, send SECURE ON first - if (this.encryptionEnabled) { - this.debugLog('Sending SECURE ON command...'); - try { - await this.makeRequest('SECURE ON', '', '', {}); - this.debugLog('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) { @@ -620,6 +707,10 @@ class WebTVClientSimulator { headers[key] = value; } } + + // Decompress the body if needed + bodyBuf = this.decompressBody(bodyBuf, headers); + if (this.currentSocket) { this.currentSocket.destroy(); this.currentSocket = null; diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/region.js b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/region.js new file mode 100644 index 00000000..6fb31ec0 --- /dev/null +++ b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/region.js @@ -0,0 +1,136 @@ +var minisrv_service_file = true; + +var timezone = "-0000"; +if (session_data.isRegistered()) { + timezone = session_data.getSessionData("timezone") || timezone; + if (request_headers.query.timezone) { + timezone = request_headers.query.timezone; + session_data.setSessionData("timezone", timezone); + } +} + +strf = strftime.timezone(timezone) + +headers = `200 OK +Connection: Keep-Alive +wtv-expire-all: wtv- +wtv-expire-all: http +wtv-client-time-zone: GMT -0000 +wtv-client-time-dst-rule: false +wtv-client-date: ${strf("%a, %d %b %Y %H:%M:%S", new Date(new Date().setUTCSeconds(new Date().getUTCSeconds())))} +Content-Type: text/html` + + + +html = ` + + +Region Settings + + + + + + +
+ + +
+ + + +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+ + + + + + + + + + + +
+ + +Region Settings + +
+ + +Current system time:

+
+Your current timezone is set to: ${timezone}

`; + + +const timezones = [ + ["UTC-12:00", "-1200"], ["UTC-11:00", "-1100"], ["UTC-10:00", "-1000"], ["UTC-09:00", "-0900"], ["UTC-08:00", "-0800"], + ["UTC-07:00", "-0700"], ["UTC-06:00", "-0600"], ["UTC-05:00", "-0500"], ["UTC-04:00", "-0400"], ["UTC-03:00", "-0300"], + ["UTC-02:00", "-0200"], ["UTC-01:00", "-0100"], ["UTC±00:00", "-0000"], ["UTC+01:00", "+0100"], ["UTC+02:00", "+0200"], + ["UTC+03:00", "+0300"], ["UTC+04:00", "+0400"], ["UTC+05:00", "+0500"], ["UTC+06:00", "+0600"], ["UTC+07:00", "+0700"], + ["UTC+08:00", "+0800"], ["UTC+09:00", "+0900"], ["UTC+10:00", "+1000"], ["UTC+11:00", "+1100"], ["UTC+12:00", "+1200"] +]; + +html += ``; + +html += `
+ + +

+


+

+

+ Zip Code Entry +

+ Zip Code: + + +

+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+
+ + +`; + +data = html; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/setup.js b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/setup.js index a2073e56..b21d1d28 100644 --- a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/setup.js +++ b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/setup.js @@ -50,9 +50,8 @@ Settings - + +
@@ -60,7 +59,7 @@ hspace=0 vspace=0 - - - - - - -
- + Settings for ${session_data.getSessionData("subscriber_username") || "You"} @@ -77,118 +76,95 @@ for ${session_data.getSessionData("subscriber_username") || "You"} -
- - -
- -Mail
-
`; - -if (minisrv_config.config.passwords) { - if (minisrv_config.config.passwords.enabled) { - data += ` -Password
-
`; - } -} - -data += ` - -Television
-
- -Text size
-
- -Music
-
`; -//printing -if (!minisrv_config.config.hide_incomplete_features) { - data += ` -Printing
-
`; -} - -data += ` - -Keyboard
-
`; - -if (session_data.user_id == 0) { - data += ` -Extra Users
-
`; -} - -data += ` - -Messenger
-
- -Dialing
-
- -Tweaks
-
- -
-Signature and more
-
-Change your password
-
-Options for your TV
-
-Make text bigger or smaller
-
-Play background songs
-
`; -// printing -if (!minisrv_config.config.hide_incomplete_features) { - data += `Change how you print
-
`; -} -data += `Choose an on-screen keyboard
`; -if (session_data.user_id == 0) { - data += `
-Add, change, or remove users
`; -} -data += `
-Configure Messenger
`; - -data += `
-Connecting to WebTV
-
-minisrv specific settings
-
-
- - -
- -
- - -
- -
- -
+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  +MailSignature and more
  +PasswordChange your password
  +TelevisionOptions for your TV
  +Text sizeMake text bigger or smaller
  +MusicPlay background songs
  +PrintingChange how you print
  +KeyboardChoose an on-screen keyboard
  +Extra UsersAdd, change, or remove users
  +MessengerConfigure Messenger
  +DialingConnecting to WebTV
  +Region SettingsChange timezone and zip code
  +Tweaksminisrv specific settings
+ + +
+ + - +   
+ `; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/classes/WTVSec.js b/zefie_wtvp_minisrv/includes/classes/WTVSec.js index c505c17e..46e9a931 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVSec.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVSec.js @@ -314,12 +314,12 @@ class WTVSec { * @param {CryptoJS.lib.WordArray|ArrayBuffer|Buffer} data Data to encrypt * @returns {ArrayBuffer} Encrypted data */ - Encrypt(keynum, data) { + Encrypt(keynum, data, reverse = false) { let session_id; if (keynum === 0) { - session_id = 0; + session_id = (reverse) ? 1 : 0; } else if (keynum === 1) { - session_id = 2; + session_id = (reverse) ? 3 : 2; } else { throw new Error("Invalid key option (0 or 1 only)"); } @@ -345,8 +345,8 @@ class WTVSec { * @returns {ArrayBuffer} Decrypted data * @notice This function is an alias for Encrypt, as WTVSec uses the same method for both encryption and decryption. */ - Decrypt(keynum, data) { - return this.Encrypt(keynum, data) + Decrypt(keynum, data, reverse = false) { + return this.Encrypt(keynum, data, reverse) } } diff --git a/zefie_wtvp_minisrv/unroll_rc4.js b/zefie_wtvp_minisrv/unroll_rc4.js deleted file mode 100644 index c58dbe18..00000000 --- a/zefie_wtvp_minisrv/unroll_rc4.js +++ /dev/null @@ -1,367 +0,0 @@ -const fs = require('fs'); -const pcapParser = require('pcap-parser'); -const WTVSec = require('./includes/classes/WTVSec.js'); -const LZPF = require('./includes/classes/LZPF.js'); -const WTVShared = require('./includes/classes/WTVShared.js')['WTVShared']; -const wtvshared = new WTVShared(); -const CryptoJS = require('crypto-js'); - -var wtvsec = null; -var wtv_challenge_response = null; -// A simple mock config, the initial_shared_key is populated dynamically. -const minisrv_config = { - config: { - keys: { - initial_shared_key: null - }, - debug_flags: { - debug: false // Set to true for verbose logging from WTVSec - } - } -}; - -// --- Main Execution --- -const pcapFile = process.argv[2]; -if (!pcapFile) { - console.error('Usage: node parse_wtvp_parser.js '); - process.exit(1); -} - -// A store for all active WTVP sessions, keyed by stream identifier. -const wtvpSessions = {}; - -const parser = pcapParser.parse(pcapFile); - -parser.on('packet', (packet) => { - const data = packet.data; - const ethType = data.readUInt16BE(12); - if (ethType !== 0x0800) return; // Not IPv4 - - // IP header parsing - const ipHeaderLength = (data[14] & 0x0F) * 4; - const ipHeader = data.slice(14, 14 + ipHeaderLength); - const protocol = ipHeader[9]; - if (protocol !== 6) return; // Not TCP - - const srcIP = ipHeader.slice(12, 16).join('.'); - const dstIP = ipHeader.slice(16, 20).join('.'); - - // TCP header parsing - const tcpHeaderStart = 14 + ipHeaderLength; - const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4; - const srcPort = data.readUInt16BE(tcpHeaderStart); - const dstPort = data.readUInt16BE(tcpHeaderStart + 2); - const seq = data.readUInt32BE(tcpHeaderStart + 4); - const flags = data[tcpHeaderStart + 13]; - const isSYN = (flags & 0x02) !== 0; - const isFIN = (flags & 0x01) !== 0; - - const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen; - const payload = data.slice(tcpPayloadOffset); - const tcpPayloadLength = payload.length; - - console.log(`[DEBUG] data.length=${data.length}, tcpPayloadOffset=${tcpPayloadOffset}`); - // Create a unique key for the TCP session - const src = `${srcIP}:${srcPort}`; - const dst = `${dstIP}:${dstPort}`; - const sessionKey = [src, dst].sort().join('-'); - - // Initialize session state if new - if (!wtvpSessions[sessionKey]) { - console.log(`[+] New TCP Session detected: ${sessionKey}`); - wtvpSessions[sessionKey] = { - clientAddr: null, - serverAddr: null, - wtvsec: null, - secureMode: false, - // TCP stream reassembly state, keyed by source ip:port - streams: {}, - }; - } - - const currentSession = wtvpSessions[sessionKey]; - - // Ensure a stream object exists for the source of this packet - if (!currentSession.streams[src]) { - currentSession.streams[src] = { nextSeq: -1, outOfOrder: {}, data: Buffer.alloc(0), isClient: false }; - } - const stream = currentSession.streams[src]; - - // 1. Identify Client and Server (if not already done) - if (!currentSession.clientAddr && payload.length > 0) { - const payloadStr = payload.toString('utf8'); - if (payloadStr.startsWith('GET') || payloadStr.startsWith('POST') || payloadStr.startsWith('SECURE ON')) { - console.log(`[*] Client identified as ${src}, Server as ${dst}`); - currentSession.clientAddr = src; - currentSession.serverAddr = dst; - - // Mark the current stream (from src) as the client - stream.isClient = true; - - // Ensure the server's stream object exists as well - if (!currentSession.streams[dst]) { - currentSession.streams[dst] = { nextSeq: -1, outOfOrder: {}, data: Buffer.alloc(0), isClient: false }; - } - } - } - - // Set the isClient flag for every packet now that identification might have happened - if(currentSession.clientAddr){ - stream.isClient = src === currentSession.clientAddr; - } - - // - - // This is the expected in-order packet. Append its payload. - stream.data = Buffer.concat([stream.data, payload]); - stream.nextSeq += tcpPayloadLength; - if (isSYN || isFIN) stream.nextSeq++; - - // Process any buffered out-of-order packets that are now in sequence - let nextSeqInChain = stream.nextSeq; - while (stream.outOfOrder[nextSeqInChain]) { - const bufferedPayload = stream.outOfOrder[nextSeqInChain]; - const bufferedPayloadLength = bufferedPayload.length; - - stream.data = Buffer.concat([stream.data, bufferedPayload]); - delete stream.outOfOrder[nextSeqInChain]; - - nextSeqInChain += bufferedPayloadLength; - } - stream.nextSeq = nextSeqInChain; - - // Now that we have new contiguous data, try to process it as application messages - for (const addr in currentSession.streams) { - const s = currentSession.streams[addr]; - if (s.data.length > 0) { - processStream(currentSession, addr); - } - } -}); - - -/** - * Processes the reassembled data buffer for a session, looking for complete messages. - * @param {object} session - The session state object. - * @param {string} sourceAddr - The source address (ip:port) of the stream being processed. - */ -function processStream(session, sourceAddr) { - const stream = session.streams[sourceAddr]; - console.log(`[DEBUG] Processing stream: ${sourceAddr} isClient: ${stream.isClient}, buffer length: ${stream.data.length}`); - - if (!stream || !session.clientAddr) return; // Don't process until client is identified - - const isClient = stream.isClient; - const direction = isClient ? '[CLIENT -> SERVER]' : '[SERVER -> CLIENT]'; - - // Loop to process all complete messages currently in the buffer - while (true) { - let buffer = stream.data; - if (buffer.length === 0) break; - if (buffer.length === 6) { - // Special case: buffer is exactly 6 bytes (likely a keepalive or unknown control message) - // Remove the 6 bytes from the buffer and continue - stream.data = buffer.slice(6); - break; - } - - const lfSeparator = Buffer.from('\n\n'); - const crlfSeparator = Buffer.from('\r\n\r\n'); - - let separatorIndex = buffer.indexOf(lfSeparator); - let separatorLength = lfSeparator.length; - const crlfIndex = buffer.indexOf(crlfSeparator); - - if (crlfIndex !== -1 && (separatorIndex === -1 || crlfIndex < separatorIndex)) { - separatorIndex = crlfIndex; - separatorLength = crlfSeparator.length; - } - - if (separatorIndex === -1) { - // Incomplete message (no full headers yet), wait for more data. - break; - } - - const headersPart = buffer.slice(0, separatorIndex); - const headers = parseHeaders(headersPart.toString('utf8')); - const headerBlockLength = separatorIndex + separatorLength; - - let messageToProcess; - let consumedSize; - - if (headers['content-length']) { - const contentLength = parseInt(headers['content-length'], 10); - const totalMessageSize = headerBlockLength + contentLength; - - if (buffer.length < totalMessageSize) { - // We have headers, but the body is not fully here yet. Wait for more data. - break; - } - messageToProcess = buffer.slice(0, totalMessageSize); - consumedSize = totalMessageSize; - } else { - // No content-length. Assume the rest of the buffer is the message. - messageToProcess = buffer.slice(0, headerBlockLength); - consumedSize = headerBlockLength; - } - - - console.log(`\n${'='.repeat(20)} Processing Message: ${direction} (${messageToProcess.length} bytes) ${'='.repeat(20)}`); - - if (!session.secureMode) { - handlePlaintext(session, messageToProcess.toString('utf8'), isClient); - } else { - handleEncrypted(session, messageToProcess, isClient); - } - - // Slice the processed message from the front of the buffer - stream.data = buffer.slice(consumedSize); - } -} - - -parser.on('end', () => { - console.log('\n[*] PCAP file processing complete.'); -}); - -parser.on('error', (err) => { - console.error(`[!] An error occurred: ${err.message}`); -}); - - -/** - * Handles a single complete plaintext WTVP message. - * @param {object} session - The session state object. - * @param {string} message - The plaintext message string. - * @param {boolean} isClient - True if the message is from the client. - */ -function handlePlaintext(session, message, isClient) { - const headers = parseHeaders(message); - if (!headers['wtv-encrypted']) { - console.log('[PLAINTEXT MESSAGE]:'); - console.log(message); - } - if (wtvsec && !session.wtvsec) { - session.wtvsec = wtvsec; - } - - - if (isClient) { - if (message.includes('SECURE ON')) { - if (session.wtvsec) { - console.log('[*] SECURE ON detected. Initializing RC4 session.'); - session.wtvsec.SecureOn(); - session.secureMode = true; - } else { - console.error('[!] SECURE ON received before wtv-initial-key. Cannot proceed.'); - } - } - if (headers['wtv-incarnation']) { - const incarnation = parseInt(headers['wtv-incarnation'], 10); - if (session.wtvsec) { - console.log(`[*] Client sent wtv-incarnation: ${incarnation}`); - session.wtvsec.set_incarnation(incarnation); - } - } - if (headers['wtv-challenge-response']) { - const challengeResponse = headers['wtv-challenge-response']; - console.log(`[*] Client sent wtv-challenge-response: ${challengeResponse}`); - if (wtv_challenge_response != challengeResponse) { - console.error('[!] Mismatched wtv-challenge-response. Expected:', wtv_challenge_response); - process.exit(1); - } else { - console.log('[*] wtv-challenge-response matches expected value.'); - } - } - } else { // Server - if (headers['wtv-initial-key']) { - const initialKey = headers['wtv-initial-key']; - console.log(`[*] Captured wtv-initial-key: ${initialKey}`); - minisrv_config.config.keys.initial_shared_key = initialKey; - wtvsec = new WTVSec(minisrv_config); - } - if (headers['wtv-challenge'] && wtvsec) { - const challenge = headers['wtv-challenge']; - console.log(`[*] Captured wtv-challenge. Processing...`); - wtv_challenge_response = wtvsec.ProcessChallenge(challenge).toString(CryptoJS.enc.Base64) - session.wtvsec = wtvsec; // Ensure session has the WTVSec instance - } - if (typeof headers['wtv-lzpf'] !== 'undefined') { - session.lzpf = true; - } - if (headers['wtv-encrypted']) { - handleEncrypted(session, Buffer.from(message), isClient); - } - } -} - -/** - * Handles a single complete encrypted WTVP message. - * @param {object} session - The session state object. - * @param {Buffer} message - The raw message buffer. - * @param {boolean} isClient - True if the message is from the client. - */ -function handleEncrypted(session, message, isClient) { - const lfSeparator = Buffer.from('\n\n'); - const crlfSeparator = Buffer.from('\r\n\r\n'); - - let separatorIndex = message.indexOf(lfSeparator); - let separatorLength = lfSeparator.length; - const crlfIndex = message.indexOf(crlfSeparator); - - if (crlfIndex !== -1 && (separatorIndex === -1 || crlfIndex < separatorIndex)) { - separatorIndex = crlfIndex; - separatorLength = crlfSeparator.length; - } - if (separatorIndex === -1) { - console.log('[!] Encrypted message without header separator. This should not happen with reassembled streams.'); - return; - } - - const headersPart = message.slice(0, separatorIndex).toString('utf8'); - const encryptedBody = message.slice(separatorIndex + separatorLength); - - console.log('[ENCRYPTED HEADERS]:'); - console.log(headersPart); - - if (encryptedBody.length > 0) { - const keyNum = isClient ? 0 : 1; - try { - let decryptedBody = session.wtvsec.Decrypt(keyNum, encryptedBody); - - // Check for compression flag in the now-decrypted headers - const headers = parseHeaders(headersPart); - if (typeof headers['wtv-lzpf'] !== 'undefined') { - console.log('\n[DECRYPTED DECOMPRESSED PAYLOAD]:'); - var lzpfHandler = new LZPF(); - decryptedBody = lzpfHandler.expand(decryptedBody); - } else { - console.log('\n[DECRYPTED PAYLOAD]:'); - } - console.log(decryptedBody.toString('utf8')); - - } catch (e) { - console.error(`[!] Decryption failed: ${e.message}`); - } - } else { - console.log('\n[Encrypted message with no body]'); - } -} - - -/** - * A utility to parse HTTP-like headers into an object. - * @param {string} payload - The raw text payload. - * @returns {object} A key-value map of the headers. - */ -function parseHeaders(payload) { - const headers = {}; - const lines = payload.split(/\r?\n/); - lines.forEach(line => { - const parts = line.split(':'); - if (parts.length >= 2) { - headers[parts[0].toLowerCase().trim()] = parts.slice(1).join(':').trim(); - } - }); - return headers; -}