new setup page (need to implement zipcode), better client_emu.js
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
136
zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/region.js
Normal file
136
zefie_wtvp_minisrv/includes/ServiceVault/wtv-setup/region.js
Normal 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±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;
|
||||
@@ -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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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>
|
||||
</SHADOW></FONT></FORM>
|
||||
<TD>
|
||||
</TABLE>
|
||||
</BODY>
|
||||
</HTML>
|
||||
|
||||
|
||||
`;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user