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:
+
+
+
+
+
+
+
+ |
+|
+ |
+ |
+|
+ |
+
+
+|
+ |
+ |
+|
+ |
+
+
+|
+ |
+ |
+|
+ |
+ |
+|
+
+ |
+ | | | |
+
+
+`;
+
+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
-
-|
- |
-|
- |
-
-
-|
- |
- |
-|
- |
-
-
-|
- |
- |
-|
- |
- | | | | | | | | |