add POST to client_sim
This commit is contained in:
@@ -19,12 +19,14 @@ const AdmZip = require('adm-zip');
|
|||||||
* using the WTVP protocol with proper authentication and service discovery.
|
* using the WTVP protocol with proper authentication and service discovery.
|
||||||
*/
|
*/
|
||||||
class WebTVClientSimulator {
|
class WebTVClientSimulator {
|
||||||
constructor(host, port, ssid, url, outputFile = null, maxRedirects = 10, useEncryption = false, request_type_download = false, debug = false, tricks = false, followImages = false, followAll = false, maxDepth = 5, maxRetries = 5, requestDelay = 250, boxType = null, username = null, keepgz = false) {
|
constructor(host, port, ssid, url, outputFile = null, maxRedirects = 10, useEncryption = false, request_type_download = false, debug = false, tricks = false, followImages = false, followAll = false, maxDepth = 5, maxRetries = 5, requestDelay = 250, boxType = null, username = null, keepgz = false, request_type_post = false, postData = null) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.ssid = ssid;
|
this.ssid = ssid;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.keepgz = keepgz;
|
this.keepgz = keepgz;
|
||||||
|
this.request_type_post = request_type_post;
|
||||||
|
this.postData = postData;
|
||||||
this.request_type_download = request_type_download;
|
this.request_type_download = request_type_download;
|
||||||
this.outputFile = outputFile;
|
this.outputFile = outputFile;
|
||||||
this.followImages = followImages;
|
this.followImages = followImages;
|
||||||
@@ -367,7 +369,7 @@ class WebTVClientSimulator {
|
|||||||
if (this.encryptionEnabled) {
|
if (this.encryptionEnabled) {
|
||||||
// For encrypted responses, we need to handle differently
|
// For encrypted responses, we need to handle differently
|
||||||
if (!responseHandled) {
|
if (!responseHandled) {
|
||||||
const result = this.handleEncryptedResponse(responseData, resolve, reject);
|
const result = this.handleEncryptedResponse(responseData, resolve, reject, false, skipRedirects);
|
||||||
if (result === true) { // If response was handled
|
if (result === true) { // If response was handled
|
||||||
responseHandled = true;
|
responseHandled = true;
|
||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
@@ -430,7 +432,7 @@ class WebTVClientSimulator {
|
|||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
// Force processing regardless of Content-Length completeness
|
// Force processing regardless of Content-Length completeness
|
||||||
try {
|
try {
|
||||||
this.handleEncryptedResponse(responseData, resolve, reject, true);
|
this.handleEncryptedResponse(responseData, resolve, reject, true, skipRedirects);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error handling encrypted response on close:', e);
|
console.error('Error handling encrypted response on close:', e);
|
||||||
reject(e);
|
reject(e);
|
||||||
@@ -628,7 +630,7 @@ class WebTVClientSimulator {
|
|||||||
/**
|
/**
|
||||||
* Handle encrypted response data
|
* Handle encrypted response data
|
||||||
*/
|
*/
|
||||||
handleEncryptedResponse(responseData, resolve, reject, forceProcess = false) {
|
handleEncryptedResponse(responseData, resolve, reject, forceProcess = false, skipRedirects = false) {
|
||||||
try {
|
try {
|
||||||
// Find header/body split using CRLF CRLF (\r\n\r\n) or fallback to LF LF (\n\n)
|
// Find header/body split using CRLF CRLF (\r\n\r\n) or fallback to LF LF (\n\n)
|
||||||
let idx = -1;
|
let idx = -1;
|
||||||
@@ -683,7 +685,7 @@ class WebTVClientSimulator {
|
|||||||
this.debugLog(`LZPF timeout - processing response with ${bodyBuffer.length} bytes`);
|
this.debugLog(`LZPF timeout - processing response with ${bodyBuffer.length} bytes`);
|
||||||
this.lzpfTimeoutId = null;
|
this.lzpfTimeoutId = null;
|
||||||
// Force processing by calling again with forceProcess = true
|
// Force processing by calling again with forceProcess = true
|
||||||
this.handleEncryptedResponse(responseData, resolve, reject, true);
|
this.handleEncryptedResponse(responseData, resolve, reject, true, false);
|
||||||
}, 100);
|
}, 100);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -747,7 +749,7 @@ class WebTVClientSimulator {
|
|||||||
// The socket will be managed by the socket pool
|
// The socket will be managed by the socket pool
|
||||||
|
|
||||||
// Check for redirects (Location header)
|
// Check for redirects (Location header)
|
||||||
if ((headers['Location'] || headers['location']) && statusLine.startsWith('302')) {
|
if ((headers['Location'] || headers['location']) && statusLine.startsWith('302') && !skipRedirects) {
|
||||||
const redirectUrl = headers['Location'] || headers['location'];
|
const redirectUrl = headers['Location'] || headers['location'];
|
||||||
this.debugLog(`Following redirect to: ${redirectUrl}`);
|
this.debugLog(`Following redirect to: ${redirectUrl}`);
|
||||||
this.redirectCount++;
|
this.redirectCount++;
|
||||||
@@ -1205,70 +1207,85 @@ class WebTVClientSimulator {
|
|||||||
*/
|
*/
|
||||||
async fetchTargetUrl() {
|
async fetchTargetUrl() {
|
||||||
console.log(`Fetching target URL: ${this.url}`);
|
console.log(`Fetching target URL: ${this.url}`);
|
||||||
if (this.useTricksAccess) {
|
|
||||||
|
// Handle special case for tricks access with POST
|
||||||
|
if (this.useTricksAccess && this.request_type_post) {
|
||||||
|
this.debugLog('Using tricks access with POST - first GET the tricks page, then POST to wtv-visit');
|
||||||
|
|
||||||
|
// First, GET the tricks page to get the wtv-visit URL
|
||||||
|
const tricksUrl = `wtv-tricks:/access?url=${encodeURIComponent(this.url)}`;
|
||||||
|
const match = tricksUrl.match(/^([\w-]+):\/?(.*)/);
|
||||||
|
if (match) {
|
||||||
|
const serviceName = match[1];
|
||||||
|
const path = '/' + (match[2] || '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// GET the tricks page (skip automatic redirects so we can handle them manually)
|
||||||
|
const tricksResult = await this.makeRequestWithRetry(serviceName, path, null, true);
|
||||||
|
|
||||||
|
// Extract wtv-visit URL or Location from headers
|
||||||
|
if (tricksResult.headers['Location'] || tricksResult.headers['location']) {
|
||||||
|
const visitUrl = tricksResult.headers['Location'] || tricksResult.headers['location'];
|
||||||
|
this.debugLog(`Got Location URL from tricks page: ${visitUrl}`);
|
||||||
|
|
||||||
|
// Now POST to the Location URL
|
||||||
|
const visitMatch = visitUrl.match(/^([\w-]+):\/?(.*)/);
|
||||||
|
if (visitMatch) {
|
||||||
|
const visitServiceName = visitMatch[1];
|
||||||
|
const visitPath = '/' + (visitMatch[2] || '');
|
||||||
|
|
||||||
|
this.debugLog(`Making POST request to Location URL: ${visitUrl} with data: ${this.postData}`);
|
||||||
|
const result = await this.makeRequestWithRetry(visitServiceName, visitPath, this.postData, false);
|
||||||
|
return this.handleTargetUrlResponse(result);
|
||||||
|
}
|
||||||
|
} else if (tricksResult.headers['wtv-visit']) {
|
||||||
|
const visitUrl = tricksResult.headers['wtv-visit'];
|
||||||
|
this.debugLog(`Got wtv-visit URL from tricks page: ${visitUrl}`);
|
||||||
|
|
||||||
|
// Now POST to the wtv-visit URL
|
||||||
|
const visitMatch = visitUrl.match(/^([\w-]+):\/?(.*)/);
|
||||||
|
if (visitMatch) {
|
||||||
|
const visitServiceName = visitMatch[1];
|
||||||
|
const visitPath = '/' + (visitMatch[2] || '');
|
||||||
|
|
||||||
|
this.debugLog(`Making POST request to wtv-visit URL: ${visitUrl} with data: ${this.postData}`);
|
||||||
|
const result = await this.makeRequestWithRetry(visitServiceName, visitPath, this.postData, false);
|
||||||
|
return this.handleTargetUrlResponse(result);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No Location or wtv-visit header found in tricks page response');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during tricks access with POST:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.useTricksAccess && !this.request_type_post) {
|
||||||
|
// Regular tricks access (GET)
|
||||||
this.debugLog('Using tricks access for target URL');
|
this.debugLog('Using tricks access for target URL');
|
||||||
this.url = `wtv-tricks:/access?url=${encodeURIComponent(this.url)}`;
|
this.url = `wtv-tricks:/access?url=${encodeURIComponent(this.url)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the target URL
|
// Parse the target URL
|
||||||
const match = this.url.match(/^([\w-]+):\/?(.*)/);
|
const match = this.url.match(/^([\w-]+):\/?(.*)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const serviceName = match[1];
|
const serviceName = match[1];
|
||||||
let path = '/' + (match[2] || '');
|
let path = '/' + (match[2] || '');
|
||||||
|
|
||||||
|
|
||||||
this.debugLog(`Parsed target service: ${serviceName}, path: ${path}`);
|
this.debugLog(`Parsed target service: ${serviceName}, path: ${path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.makeRequestWithRetry(serviceName, path, null, false);
|
// Determine if this should be a POST request
|
||||||
|
const requestData = this.request_type_post ? this.postData : null;
|
||||||
|
if (this.request_type_post) {
|
||||||
// Handle the response
|
this.debugLog(`Making POST request to ${serviceName}:${path} with data: ${requestData}`);
|
||||||
if (result.body) {
|
|
||||||
this.debugLog('\n*** Target URL Response Body ***');
|
|
||||||
if (this.outputFile) {
|
|
||||||
// Check if target URL returned a download-list and --follow is enabled
|
|
||||||
const contentType = result.headers['content-type'] || '';
|
|
||||||
const normalizedContentType = contentType.toLowerCase().split(';')[0].trim();
|
|
||||||
const isDownloadList = normalizedContentType === 'wtv/download-list';
|
|
||||||
|
|
||||||
if (this.followAll) {
|
|
||||||
// Store the main content first
|
|
||||||
this.storeContent(this.url, result);
|
|
||||||
|
|
||||||
// Process all pending downloads
|
|
||||||
await this.processAllDownloads();
|
|
||||||
|
|
||||||
// Create comprehensive archive
|
|
||||||
await this.createComprehensiveArchive();
|
|
||||||
} else if (this.followImages && isDownloadList) {
|
|
||||||
this.debugLog('Target URL returned download-list content with --follow enabled, creating archive...');
|
|
||||||
await this.createDownloadListArchive(result.body, result.headers);
|
|
||||||
} else if (this.followImages) {
|
|
||||||
await this.saveToFile(result.body, result.headers);
|
|
||||||
} else {
|
|
||||||
await this.saveToFile(result.body, result.headers);
|
|
||||||
}
|
|
||||||
console.log(`Content saved to: ${this.outputFile}`);
|
|
||||||
} else {
|
|
||||||
// Detect text content for CLI output
|
|
||||||
const contentType = result.headers['content-type'] || '';
|
|
||||||
if (/^text\//.test(contentType) || /json|xml|javascript||download-list/.test(contentType) || contentType === "x-wtv-addresses") {
|
|
||||||
console.log(result.body.toString('utf8'));
|
|
||||||
} else if (result.body.length === 0) {
|
|
||||||
console.log('<empty response>');
|
|
||||||
} else {
|
|
||||||
console.log('<binary data>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.debugLog('No body content received from target URL');
|
this.debugLog(`Making GET request to ${serviceName}:${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debugLog('\n*** Request completed successfully ***');
|
const result = await this.makeRequestWithRetry(serviceName, path, requestData, false);
|
||||||
this.cleanup();
|
return this.handleTargetUrlResponse(result);
|
||||||
process.exit(0);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching target URL:', error);
|
console.error('Error fetching target URL:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -1278,6 +1295,59 @@ class WebTVClientSimulator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the response from the target URL
|
||||||
|
*/
|
||||||
|
async handleTargetUrlResponse(result) {
|
||||||
|
// Handle the response
|
||||||
|
if (result.body) {
|
||||||
|
this.debugLog('\n*** Target URL Response Body ***');
|
||||||
|
if (this.outputFile) {
|
||||||
|
// Check if target URL returned a download-list and --follow is enabled
|
||||||
|
const contentType = result.headers['content-type'] || '';
|
||||||
|
const normalizedContentType = contentType.toLowerCase().split(';')[0].trim();
|
||||||
|
const isDownloadList = normalizedContentType === 'wtv/download-list';
|
||||||
|
|
||||||
|
if (this.followAll) {
|
||||||
|
// Store the main content first
|
||||||
|
this.storeContent(this.url, result);
|
||||||
|
|
||||||
|
// Process all pending downloads
|
||||||
|
await this.processAllDownloads();
|
||||||
|
|
||||||
|
// Create comprehensive archive
|
||||||
|
await this.createComprehensiveArchive();
|
||||||
|
} else if (this.followImages && isDownloadList) {
|
||||||
|
this.debugLog('Target URL returned download-list content with --follow enabled, creating archive...');
|
||||||
|
await this.createDownloadListArchive(result.body, result.headers);
|
||||||
|
} else if (this.followImages) {
|
||||||
|
await this.saveToFile(result.body, result.headers);
|
||||||
|
} else {
|
||||||
|
await this.saveToFile(result.body, result.headers);
|
||||||
|
}
|
||||||
|
console.log(`Content saved to: ${this.outputFile}`);
|
||||||
|
} else {
|
||||||
|
// Detect text content for CLI output
|
||||||
|
const contentType = result.headers['content-type'] || '';
|
||||||
|
if (/^text\//.test(contentType) || /json|xml|javascript||download-list/.test(contentType) || contentType === "x-wtv-addresses") {
|
||||||
|
console.log(result.body.toString('utf8'));
|
||||||
|
} else if (result.body.length === 0) {
|
||||||
|
console.log('<empty response>');
|
||||||
|
} else {
|
||||||
|
console.log('<binary data>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.debugLog('No body content received from target URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.debugLog('\n*** Request completed successfully ***');
|
||||||
|
this.cleanup();
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the content response without following redirects
|
* Process the content response without following redirects
|
||||||
*/
|
*/
|
||||||
@@ -2613,7 +2683,9 @@ function parseArgs() {
|
|||||||
requestDelay: 250,
|
requestDelay: 250,
|
||||||
debug: false,
|
debug: false,
|
||||||
username: null,
|
username: null,
|
||||||
keepgz: false
|
keepgz: false,
|
||||||
|
request_type_post: false,
|
||||||
|
postData: null
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
@@ -2696,6 +2768,14 @@ function parseArgs() {
|
|||||||
case '--keepgz':
|
case '--keepgz':
|
||||||
config.keepgz = true;
|
config.keepgz = true;
|
||||||
break;
|
break;
|
||||||
|
case '--post':
|
||||||
|
config.request_type_post = true;
|
||||||
|
break;
|
||||||
|
case '--data':
|
||||||
|
if (i + 1 < args.length) {
|
||||||
|
config.postData = args[++i];
|
||||||
|
}
|
||||||
|
break;
|
||||||
case '--help':
|
case '--help':
|
||||||
console.log(`
|
console.log(`
|
||||||
WebTV Client Simulator
|
WebTV Client Simulator
|
||||||
@@ -2720,6 +2800,8 @@ Options:
|
|||||||
--retries <num> Maximum number of retries for ECONNREFUSED errors (default: 5)
|
--retries <num> Maximum number of retries for ECONNREFUSED errors (default: 5)
|
||||||
--delay <num> Delay between requests in milliseconds (default: 250)
|
--delay <num> Delay between requests in milliseconds (default: 250)
|
||||||
--keepgz Keep .gz files compressed when following wtv/download-list (default: false)
|
--keepgz Keep .gz files compressed when following wtv/download-list (default: false)
|
||||||
|
--post Use POST method for the final target URL request
|
||||||
|
--data <data> POST data to send with --post requests (required with --post)
|
||||||
--debug Enable debug logging
|
--debug Enable debug logging
|
||||||
--help Show this help message
|
--help Show this help message
|
||||||
|
|
||||||
@@ -2727,11 +2809,19 @@ Example:
|
|||||||
node client_emu.js --host 192.168.1.100 --port 1615 --ssid 8100000000000001 --url wtv-home:/home --file output.html
|
node client_emu.js --host 192.168.1.100 --port 1615 --ssid 8100000000000001 --url wtv-home:/home --file output.html
|
||||||
node client_emu.js --host 127.0.0.1 --url wtv-home:/home --file archive.zip --follow --debug
|
node client_emu.js --host 127.0.0.1 --url wtv-home:/home --file archive.zip --follow --debug
|
||||||
node client_emu.js --host 127.0.0.1 --url wtv-home:/home --file complete.zip --follow-all --depth 2 --debug
|
node client_emu.js --host 127.0.0.1 --url wtv-home:/home --file complete.zip --follow-all --depth 2 --debug
|
||||||
|
node client_emu.js --host 127.0.0.1 --url wtv-mail:/sendmail --post --data "to=user@example.com&subject=test&body=Hello" --file response.html
|
||||||
|
node client_emu.js --host 127.0.0.1 --url wtv-mail:/sendmail --post --data "to=user@example.com&subject=test&body=Hello" --tricks
|
||||||
`);
|
`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate POST requirements
|
||||||
|
if (config.request_type_post && !config.postData) {
|
||||||
|
console.error('Error: --post requires --data to be specified');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2740,7 +2830,7 @@ Example:
|
|||||||
*/
|
*/
|
||||||
async function main() {
|
async function main() {
|
||||||
const config = parseArgs();
|
const config = parseArgs();
|
||||||
const simulator = new WebTVClientSimulator(config.host, config.port, config.ssid, config.url, config.outputFile, config.maxRedirects, config.useEncryption, config.request_type_download, config.debug, config.useTricksAccess, config.followImages, config.followAll, config.maxDepth, config.maxRetries, config.requestDelay, config.boxType, config.username, config.keepgz);
|
const simulator = new WebTVClientSimulator(config.host, config.port, config.ssid, config.url, config.outputFile, config.maxRedirects, config.useEncryption, config.request_type_download, config.debug, config.useTricksAccess, config.followImages, config.followAll, config.maxDepth, config.maxRetries, config.requestDelay, config.boxType, config.username, config.keepgz, config.request_type_post, config.postData);
|
||||||
|
|
||||||
// Handle graceful shutdown
|
// Handle graceful shutdown
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user