new setup page (need to implement zipcode), better client_emu.js

This commit is contained in:
zefie
2025-08-09 20:14:08 -04:00
parent 709a282abd
commit f89dd38b13
6 changed files with 373 additions and 537 deletions

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 = `<HTML>
<HEAD>
<TITLE>
Region Settings
</TITLE>
<DISPLAY nosave skipback noscroll>
</HEAD>
<sidebar width=110> <table cellspacing=0 cellpadding=0 BGCOLOR=452a36>
<tr>
<td colspan=3 abswidth=104 absheight=4>
<td rowspan=99 width=6 absheight=420 valign=top align=left>
<img src="file://ROM/Cache/Shadow.gif" width=6 height=420>
<tr>
<td abswidth=6>
<td abswidth=92 absheight=76>
<table absheight=76 cellspacing=0 cellpadding=0>
<tr>
<td align=right>
<img src="${minisrv_config.config.service_logo}" width=87 height=67>
</table>
<td abswidth=6>
<tr><td absheight=5 colspan=3>
<table cellspacing=0 cellpadding=0>
<tr><td abswidth=104 absheight=2 valign=middle align=center bgcolor=2e1e26>
<spacer>
<tr><td abswidth=104 absheight=1 valign=top align=left>
<tr><td abswidth=104 absheight=2 valign=top align=left bgcolor=6b4657>
<spacer>
</table>
<tr><td absheight=132>
<tr><td absheight=166 align=right colspan=3>
<img src="ROMCache/SettingsBanner.gif" width=54 height=166>
<tr><td absheight=41>
</table>
</sidebar>
<BODY BGCOLOR="#191919" TEXT="#44cc55" LINK="189CD6" VLINK="189CD6" HSPACE=0 VSPACE=0 FONTSIZE="large">
<table cellspacing=0 cellpadding=0>
<tr>
<td abswidth=14>
<td abswidth=416 absheight=80 valign=center>
<font size="+2" color="E7CE4A"><blackface><shadow>
Region Settings
<td abswidth=20>
<tr>
<td>
<td absheight=244 valign=top align=left>
Current system time: <clock></clock><br><br>
<form action="wtv-setup:/region" method="post">
Your current timezone is set to: <b>${timezone}</b><br><br>`;
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&#177;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 += `<select name="timezone" onchange="this.form.submit()">\n`;
for (const tz of timezones) {
html += ` <option value="${tz[1]}" ${tz[1] === timezone ? 'selected' : ''}>${tz[0]}</option>\n`;
}
html += `</select>`;
html += `</form>
<p>
<hr>
<p>
<form action="submit" method="post">
<b>Zip Code Entry</b>
<p>
Zip Code:
<input type="text" name="zip" size="10" maxlength="5">
<input type="submit" value="Submit">
</form>
<TR>
<TD>
<TD COLSPAN=4 HEIGHT=0 VALIGN=top ALIGN=left>
<tr>
<TD>
<td colspan=3 height=2 valign=middle align=center bgcolor="2B2B2B">
<spacer type=block width=436 height=1>
<tr>
<TD>
<td colspan=4 height=1 valign=top align=left>
<tr>
<TD>
<td colspan=3 height=2 valign=top align=left bgcolor="0D0D0D">
<spacer type=block width=436 height=1>
<TR>
<TD>
<TD COLSPAN=4 HEIGHT=4 VALIGN=top ALIGN=left>
<TR>
<TD>
<TD COLSPAN=2 VALIGN=top ALIGN=left>
<tr>
<TD COLSPAN=2 VALIGN=top ALIGN=right>
<FORM action="wtv-setup:/setup">
<FONT COLOR="#E7CE4A" SIZE=-1><SHADOW>
<INPUT TYPE=SUBMIT BORDERIMAGE="file://ROM/Borders/ButtonBorder2.bif" Value=Done NAME="Done" USESTYLE WIDTH=103>
</SHADOW></FONT></FORM>
<TD>
</TABLE>
</BODY>
</HTML>
`;
data = html;

View File

@@ -50,9 +50,8 @@ Settings
<tr><td absheight=41>
</table>
</sidebar>
<BODY BGCOLOR="#191919" TEXT="#42CC55" LINK="36d5ff" VLINK="36d5ff" FONTSIZE="large"
hspace=0 vspace=0
>
<BODY BGCOLOR="#191919" TEXT="#42CC55" LINK="36d5ff" VLINK="36d5ff" FONTSIZE="small" hspace=0 vspace=0>
<table cellspacing=0 cellpadding=0>
<tr>
<td abswidth=14>
@@ -60,7 +59,7 @@ hspace=0 vspace=0
<table cellspacing=0 cellpadding=0>
<tr>
<td valign=center absheight=80>
<shadow><blackface><font color="e7ce4a" font size="+1">
<shadow><blackface><font color="e7ce4a" font size="5">
Settings
for ${session_data.getSessionData("subscriber_username") || "You"}
</font><blackface><shadow>
@@ -77,118 +76,95 @@ for ${session_data.getSessionData("subscriber_username") || "You"}
<td colspan=4 height=2 valign=top align=left bgcolor="0D0D0D">
<spacer type=block width=436 height=1>
<td abswidth=20>
<TR>
<td>
<font size="-1">
<td WIDTH=150 HEIGHT=244 VALIGN=top ALIGN=left>
<br><font size="-1"><blackface>
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/mail">Mail</a><BR>
<spacer type=block width=1 height=5><BR>`;
if (minisrv_config.config.passwords) {
if (minisrv_config.config.passwords.enabled) {
data += `<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/edit-password">Password</a><BR>
<spacer type=block width=1 height=5><BR>`;
}
}
data += `
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/screen">Television</a><BR>
<spacer type=block width=1 height=5><BR>
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/text">Text size</a><BR>
<spacer type=block width=1 height=5><BR>
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/sound">Music</a><BR>
<spacer type=block width=1 height=5><BR>`;
//printing
if (!minisrv_config.config.hide_incomplete_features) {
data += `<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="${notImplementedAlert}"><strike>Printing</strike></a><BR>
<spacer type=block width=1 height=5><BR>`;
}
data += `
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/keyboard">Keyboard</a><BR>
<spacer type=block width=1 height=5><BR>`;
if (session_data.user_id == 0) {
data += `<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/accounts">Extra Users</a><BR>
<spacer type=block width=1 height=5><BR>`;
}
data += `
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/messenger">Messenger</a><BR>
<spacer type=block width=1 height=5><BR>
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/phone">Dialing</a><BR>
<spacer type=block width=1 height=5><BR>
<img src="ROMCache/BulletArrow.gif" width=6 height=13 valign=absmiddle><spacer type=block width=6 height=1>
<a href="wtv-setup:/tweaks">Tweaks</a><BR>
<TD WIDTH=20>
<TD WIDTH=300 VALIGN=top ALIGN=left>
<spacer type=block width=6 height=14><font size="2"><br>
Signature <strike>and more</strike><BR>
<spacer type=block width=6 height=5><font size="2"><br>
Change your password<BR>
<spacer type=block width=6 height=5><font size="2"><br>
Options for your TV<BR>
<spacer type=block width=6 height=5><font size="2"><br>
Make text bigger or smaller<BR>
<spacer type=block width=6 height=5><font size="2"><br>
Play background songs<BR>
<spacer type=block width=6 height=5><font size="2"><br>`;
// printing
if (!minisrv_config.config.hide_incomplete_features) {
data += `<strike>Change how you print</strike><BR>
<spacer type=block width=6 height=5><font size="2"><br>`;
}
data += `Choose an on-screen keyboard<BR>`;
if (session_data.user_id == 0) {
data += `<spacer type=block width=6 height=5><font size="2"><br>
Add, change, or remove users<BR>`;
}
data += `<spacer type=block width=6 height=5><font size="2"><br>
Configure Messenger<BR>`;
data += `<spacer type=block width=6 height=6><font size="2"><br>
Connecting to WebTV<BR>
<spacer type=block width=6 height=6><font size="2"><br>
minisrv specific settings<BR>
<tr>
<td colspan=4 height=2>
<tr>
<TD>
<td colspan=4 height=2 valign=middle align=center bgcolor="2B2B2B">
<spacer type=block width=436 height=1>
<tr>
<TD>
<td colspan=4 height=1 valign=top align=left>
<tr>
<TD>
<td colspan=4 height=2 valign=top align=left bgcolor="0D0D0D">
<spacer type=block width=436 height=1>
<TR>
<TD>
<TD COLSPAN=4 HEIGHT=4 VALIGN=top ALIGN=left>
<TR>
<TD>
<TD COLSPAN=3 VALIGN=top ALIGN=right>
<FORM
action="wtv-home:/home" selected>
<table cellspacing=0 cellpadding=2>
<br><br>
<tr>
<td width=20>&nbsp;</td>
<td width=160><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/mail"><font size=2>Mail</a></td>
<td width=220><font size=2>Signature <strike>and more</strike></td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/edit-password"><font size=2>Password</a></td>
<td><font size=2>Change your password</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/screen"><font size=2>Television</a></td>
<td><font size=2>Options for your TV</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/text"><font size=2>Text size</a></td>
<td><font size=2>Make text bigger or smaller</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/sound"><font size=2>Music</a></td>
<td><font size=2>Play background songs</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="${notImplementedAlert}"><font size=2><strike>Printing</strike></a></td>
<td><strike><font size=2>Change how you print</strike></td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/keyboard"><font size=2>Keyboard</a></td>
<td><font size=2>Choose an on-screen keyboard</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/accounts"><font size=2>Extra Users</a></td>
<td><font size=2>Add, change, or remove users</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/messenger"><font size=2>Messenger</a></td>
<td><font size=2>Configure Messenger</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/phone"><font size=2>Dialing</a></td>
<td><font size=2>Connecting to WebTV</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/region"><font size=2>Region Settings</a></td>
<td><font size=2>Change timezone and zip code</td>
</tr>
<tr>
<td width=20>&nbsp;</td>
<td><img src="ROMCache/BulletArrow.gif" width=6 height=6 valign=absmiddle>
<a href="wtv-setup:/tweaks"><font size=2>Tweaks</a></td>
<td><font size=2>minisrv specific settings</td>
</tr>
</table>
<table width=100%>
<tr><td align=right>
<spacer type=block width=436 height=4>
<FORM action="wtv-home:/home" selected>
<FONT COLOR="#E7CE4A" SIZE=-1><SHADOW>
<INPUT TYPE=SUBMIT BORDERIMAGE="file://ROM/Borders/ButtonBorder2.bif" Value=Done NAME="Done" USESTYLE WIDTH=103>
<INPUT TYPE=SUBMIT BORDERIMAGE="file://ROM/Borders/ButtonBorder2.bif" Value=Done NAME="Done" USESTYLE WIDTH=103>&nbsp;&nbsp;&nbsp;
</SHADOW></FONT></FORM>
<TD>
</TABLE>
</BODY>
</HTML>
`;

View File

@@ -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)
}
}

View File

@@ -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 <path_to_pcap_file>');
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;
}