support login flow with multi user and/or password (minisrv)

This commit is contained in:
zefie
2025-08-10 19:44:40 -04:00
parent 26bd129e38
commit 393067e1fc

View File

@@ -32,6 +32,7 @@ class WebTVClientSimulator {
this.maxRetries = maxRetries; this.maxRetries = maxRetries;
this.requestDelay = requestDelay; this.requestDelay = requestDelay;
this.currentDepth = 0; this.currentDepth = 0;
this.currentwtvsec = null;
this.authenticated = false; this.authenticated = false;
this.downloadedUrls = new Set(); // Track what we've already downloaded this.downloadedUrls = new Set(); // Track what we've already downloaded
this.pendingDownloads = []; // Queue of {url, referrer} objects to download this.pendingDownloads = []; // Queue of {url, referrer} objects to download
@@ -350,6 +351,12 @@ class WebTVClientSimulator {
responseData = Buffer.concat([responseData, chunk]); responseData = Buffer.concat([responseData, chunk]);
this.debugLog(`Received chunk: ${chunk.length} bytes (total: ${responseData.length} bytes)`); this.debugLog(`Received chunk: ${chunk.length} bytes (total: ${responseData.length} bytes)`);
// Debug: Show received data for POST requests
if (data) {
this.debugLog('POST Response chunk:', chunk.toString('utf8').substring(0, 200) + (chunk.length > 200 ? '...' : ''));
this.debugLog('POST Response hex dump:', chunk.toString('hex').substring(0, 100) + (chunk.length > 50 ? '...' : ''));
}
// Clear any existing timeout for LZPF completion detection // Clear any existing timeout for LZPF completion detection
if (this.lzpfTimeoutId) { if (this.lzpfTimeoutId) {
clearTimeout(this.lzpfTimeoutId); clearTimeout(this.lzpfTimeoutId);
@@ -375,9 +382,24 @@ class WebTVClientSimulator {
const lflf = Buffer.from('\n\n'); const lflf = Buffer.from('\n\n');
let idx = responseData.indexOf(crlfcrlf); let idx = responseData.indexOf(crlfcrlf);
if (idx === -1) idx = responseData.indexOf(lflf); if (idx === -1) idx = responseData.indexOf(lflf);
if (idx !== -1 && !responseHandled) {
// For POST requests, also check if we have what looks like a complete HTTP response
// Detect either HTTP-style or WTV-style status lines
const headSample = responseData.toString('utf8', 0, Math.min(32, responseData.length));
const hasHttpStatus = /^(?:HTTP\/[\d\.]+ \d+)/.test(headSample);
const hasWtvStatus = /^(?:\d{3}\s)/.test(headSample); // e.g., '200 OK' without HTTP/
// Debug: Show detection status for POST requests
if (data) {
this.debugLog(`POST response detection: headerSeparator=${idx !== -1}, httpStatus=${hasHttpStatus || hasWtvStatus}, totalBytes=${responseData.length}`);
}
if ((idx !== -1 || (data && (hasHttpStatus || hasWtvStatus) && responseData.length > 20)) && !responseHandled) {
responseHandled = true; responseHandled = true;
this.debugLog('Complete response detected, processing...'); this.debugLog('Complete response detected, processing...');
if (data && idx === -1) {
this.debugLog('POST response without standard header separator detected');
}
cleanupListeners(); cleanupListeners();
socket._inUse = false; socket._inUse = false;
this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl, socket, socketKey); this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl, socket, socketKey);
@@ -390,17 +412,57 @@ class WebTVClientSimulator {
this.socketPool.delete(socketKey); this.socketPool.delete(socketKey);
socket._inUse = false; socket._inUse = false;
if (responseData.length > 0 && !this.encryptionEnabled && !responseHandled) { // Clear any pending LZPF timer
if (this.lzpfTimeoutId) {
clearTimeout(this.lzpfTimeoutId);
this.lzpfTimeoutId = null;
}
if (responseData.length > 0 && !responseHandled) {
// Only process if not already processed // Only process if not already processed
const crlfcrlf = Buffer.from('\r\n\r\n'); if (this.encryptionEnabled) {
const lflf = Buffer.from('\n\n'); // For encrypted sessions, force processing on close
let idx = responseData.indexOf(crlfcrlf); this.debugLog('Processing encrypted response on connection close...');
if (idx === -1) idx = responseData.indexOf(lflf); if (data) {
if (idx === -1) { this.debugLog('POST response (enc) received on close:', responseData.toString('utf8', 0, Math.min(500, responseData.length)) + (responseData.length > 500 ? '...' : ''));
}
responseHandled = true; responseHandled = true;
this.debugLog('Processing incomplete response on close...');
cleanupListeners(); cleanupListeners();
this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl, socket, socketKey); // Force processing regardless of Content-Length completeness
try {
this.handleEncryptedResponse(responseData, resolve, reject, true);
} catch (e) {
console.error('Error handling encrypted response on close:', e);
reject(e);
}
} else {
this.debugLog('Processing response on connection close...');
if (data) {
this.debugLog('POST response received on close:', responseData.toString('utf8').substring(0, 500) + (responseData.length > 500 ? '...' : ''));
this.debugLog('POST response hex on close:', responseData.toString('hex').substring(0, 200) + (responseData.length > 100 ? '...' : ''));
}
const crlfcrlf = Buffer.from('\r\n\r\n');
const lflf = Buffer.from('\n\n');
let idx = responseData.indexOf(crlfcrlf);
if (idx === -1) idx = responseData.indexOf(lflf);
if (idx === -1) {
responseHandled = true;
this.debugLog('Processing incomplete response on close...');
cleanupListeners();
this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl, socket, socketKey);
} else {
responseHandled = true;
this.debugLog('Processing complete response on close...');
cleanupListeners();
this.handleResponse(responseData, resolve, reject, skipRedirects, currentUrl, socket, socketKey);
}
}
} else if (responseData.length === 0) {
this.debugLog('Connection closed with no response data');
if (data) {
console.warn('POST request received no response from server');
this.debugLog('POST request that got no response was to:', currentUrl || 'unknown URL');
} }
} }
}; };
@@ -414,19 +476,23 @@ class WebTVClientSimulator {
}; };
const handleTimeout = () => { const handleTimeout = () => {
console.error('Request timed out'); if (data) {
console.error(`POST request timed out after 10 seconds - server not responding to: ${currentUrl}`);
} else {
console.error('Request timed out');
}
socket.destroy(); socket.destroy();
this.socketPool.delete(socketKey); this.socketPool.delete(socketKey);
socket._inUse = false; socket._inUse = false;
cleanupListeners(); cleanupListeners();
reject(new Error('Request timeout')); reject(new Error(data ? 'POST request timeout - server not responding' : 'Request timeout'));
}; };
// Set up event listeners // Set up event listeners
socket.on('data', handleData); socket.on('data', handleData);
socket.on('close', handleClose); socket.on('close', handleClose);
socket.on('error', handleError); socket.on('error', handleError);
socket.setTimeout(30000, handleTimeout); socket.setTimeout(data ? 10000 : 30000, handleTimeout); // Shorter timeout for POST requests
if (isNewConnection) { if (isNewConnection) {
socket.connect(targetPort, targetHost, () => { socket.connect(targetPort, targetHost, () => {
@@ -462,7 +528,6 @@ class WebTVClientSimulator {
request += `wtv-connect-session-id: ${this.connectSessionId}\r\n` request += `wtv-connect-session-id: ${this.connectSessionId}\r\n`
// Add additional headers that real client sends (from PCAP analysis) // Add additional headers that real client sends (from PCAP analysis)
request += `User-Agent: Mozilla/4.0 WebTV/2.2.6.1 (compatible; MSIE 4.0)\r\n`;
if (this.useEncryption) request += `wtv-encryption: true\r\n`; if (this.useEncryption) request += `wtv-encryption: true\r\n`;
if (!this.challengeResponse) request += `wtv-script-id: -1896417432\r\n`; if (!this.challengeResponse) request += `wtv-script-id: -1896417432\r\n`;
if (!this.challengeResponse) request += `wtv-script-mod: 1754789923\r\n`; if (!this.challengeResponse) request += `wtv-script-mod: 1754789923\r\n`;
@@ -484,8 +549,8 @@ class WebTVClientSimulator {
// Add content if POST // Add content if POST
if (data) { if (data) {
const content = typeof data === 'string' ? data : JSON.stringify(data); const content = typeof data === 'string' ? data : JSON.stringify(data);
request += `Content-Length: ${content.length}\r\n`;
request += `Content-Type: application/x-www-form-urlencoded\r\n`; request += `Content-Type: application/x-www-form-urlencoded\r\n`;
request += `Content-Length: ${content.length}\r\n`;
request += `\r\n${content}`; request += `\r\n${content}`;
} else { } else {
request += '\r\n'; request += '\r\n';
@@ -541,8 +606,8 @@ class WebTVClientSimulator {
// Add content if POST // Add content if POST
if (data) { if (data) {
const content = typeof data === 'string' ? data : JSON.stringify(data); const content = typeof data === 'string' ? data : JSON.stringify(data);
request += `Content-Length: ${content.length}\r\n`;
request += `Content-Type: application/x-www-form-urlencoded\r\n`; request += `Content-Type: application/x-www-form-urlencoded\r\n`;
request += `Content-Length: ${content.length}\r\n`;
request += `\r\n${content}`; request += `\r\n${content}`;
} else { } else {
request += '\r\n'; request += '\r\n';
@@ -704,9 +769,6 @@ class WebTVClientSimulator {
} }
} }
handleResponse(responseData, resolve, reject, skipRedirects = false, currentUrl = null, socket = null, socketKey = null) { handleResponse(responseData, resolve, reject, skipRedirects = false, currentUrl = null, socket = null, socketKey = null) {
this.debugLog('\nReceived response:');
this.debugLog(responseData);
// Update previousUrl for next request's Referer header // Update previousUrl for next request's Referer header
if (currentUrl) { if (currentUrl) {
this.previousUrl = currentUrl; this.previousUrl = currentUrl;
@@ -755,6 +817,7 @@ class WebTVClientSimulator {
} }
} }
} }
this.processHeaders(headers); this.processHeaders(headers);
this.debugLog("srv headers:", headers); this.debugLog("srv headers:", headers);
@@ -768,15 +831,22 @@ class WebTVClientSimulator {
// Decrypt the content if we have encryption enabled // Decrypt the content if we have encryption enabled
if (this.wtvsec && bodyBuf.length > 0) { if (this.wtvsec && bodyBuf.length > 0) {
try { try {
this.debugLog('Decrypting login-stage-two content...'); // Only decrypt if not already decrypted before processing headers
this.wtvsec.set_incarnation(this.incarnation); // Ensure WTVSec has the correct incarnation if (this.wtvsec && this.useEncryption && headers['wtv-encrypted']) {
const decryptedBuffer = this.wtvsec.Decrypt(1, bodyBuf); this.debugLog('Decrypting login-stage-two content...');
bodyBuf = Buffer.from(decryptedBuffer); this.wtvsec.set_incarnation(this.incarnation); // Ensure WTVSec has the correct incarnation
this.debugLog(`Content decrypted successfully: ${bodyBuf.length} bytes`); const decryptedBuffer = this.wtvsec.Decrypt(1, bodyBuf);
bodyBuf = Buffer.from(decryptedBuffer);
this.debugLog(`Content decrypted successfully: ${bodyBuf.length} bytes`);
}
// Re-decompress after decryption in case it was compressed // Re-decompress after decryption in case it was compressed
bodyBuf = this.decompressBody(bodyBuf, headers); bodyBuf = this.decompressBody(bodyBuf, headers);
this.debugLog(`Final content after decrypt+decompress: ${bodyBuf.length} bytes`); this.debugLog(`Final content after decrypt+decompress: ${bodyBuf.length} bytes`);
// Debug: Show first 200 chars of decrypted content for verification
this.debugLog('Login-stage-two decrypted content preview:');
this.debugLog(bodyBuf.toString('utf8').substring(0, 200));
} catch (error) { } catch (error) {
console.error('Error decrypting login-stage-two content:', error); console.error('Error decrypting login-stage-two content:', error);
} }
@@ -801,15 +871,95 @@ class WebTVClientSimulator {
// The parseLoginStageTwoHTML method already handles displaying users and exiting // The parseLoginStageTwoHTML method already handles displaying users and exiting
} }
} }
} else if (currentUrl && currentUrl.startsWith("wtv-head-waiter:/choose-user")) {
// minisrv
const contentLength = headers['content-length'];
if (contentLength && parseInt(contentLength) > 0) {
this.debugLog('Special handling for wtv-head-waiter:/choose-user with content-length > 0');
this.debugLog(`Content-Length: ${contentLength}`);
if (this.wtvsec && bodyBuf.length > 0) {
try {
bodyBuf = this.decompressBody(bodyBuf, headers);
const parseResult = this.parseMinisrvChooseUser(bodyBuf);
if (parseResult.selectedUser) {
// User was specified and found, automatically follow their link
this.debugLog(`Following link for user: ${parseResult.selectedUser.username}`);
setTimeout(() => {
this.followVisit(parseResult.selectedUser.href)
.then(resolve)
.catch(reject);
}, this.requestDelay);
return; // Exit early to follow the user's link
}
} catch (error) {
console.error('Error parsing choose-user HTML:', error);
}
}
}
} else if (currentUrl && (currentUrl.startsWith("wtv-head-waiter:/VLN-stage-two") || currentUrl.startsWith("wtv-head-waiter:/password"))) {
const contentLength = headers['content-length'];
if (contentLength && parseInt(contentLength) > 0) {
this.debugLog('Special handling for wtv-head-waiter:/VLN-stage-two with content-length > 0 (password)');
this.debugLog(`Content-Length: ${contentLength}`);
if (this.wtvsec && bodyBuf.length > 0) {
try {
// Only decrypt if not already decrypted before processing headers
if (this.wtvsec && this.useEncryption && headers['wtv-encrypted']) {
this.debugLog('Decrypting VLN-stage-two content...');
this.wtvsec.set_incarnation(this.incarnation); // Ensure WTVSec has the correct incarnation
const decryptedBuffer = this.wtvsec.Decrypt(1, bodyBuf);
bodyBuf = Buffer.from(decryptedBuffer);
this.debugLog(`Content decrypted successfully: ${bodyBuf.length} bytes`);
}
// Decompress after decryption if needed
bodyBuf = this.decompressBody(bodyBuf, headers);
this.debugLog(`Final content after decrypt+decompress: ${bodyBuf.length} bytes`);
// Debug: Show first 500 chars of decrypted content
this.debugLog('VLN-stage-two decrypted content preview:');
this.debugLog(bodyBuf.toString('utf8').substring(0, 500));
} catch (error) {
console.error('Error decrypting VLN-stage-two content:', error);
}
}
// Parse the HTML to extract usernames and their href links
if (bodyBuf.length > 0) {
const parseResult = this.parseVLNStageTwoHTML(bodyBuf);
if (parseResult.formData && parseResult.formAction) {
// Prepare form data as application/x-www-form-urlencoded
const formBody = parseResult.formData
? Object.entries(parseResult.formData)
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
.join('&')
: '';
// Make POST request to formAction with formBody
this.debugLog(`Submitting VLN-stage-two form to ${parseResult.formAction} with password`);
this.makeRequestWithRetry(
parseResult.formAction.split(':')[0],
parseResult.formAction.replace(/^[^:]+:/, ''),
formBody,
false
).catch(err => {
console.error('Error submitting VLN-stage-two form:', err);
this.cleanup();
process.exit(1);
});
}
}
}
} else { } else {
if (this.encryptionEnabled && bodyBuf.length > 0) { // Only decrypt if not already decrypted before processing headers
// We decrypt here if we have encryption enabled but the content wasn't already decrypted
if (this.encryptionEnabled && this.wtvsec && bodyBuf.length > 0) {
this.debugLog('Decrypting response body...'); this.debugLog('Decrypting response body...');
bodyBuf = this.wtvsec.Decrypt(1, bodyBuf); bodyBuf = this.wtvsec.Decrypt(1, bodyBuf);
} }
// Decompress the body if needed // Decompress the body if needed
bodyBuf = this.decompressBody(bodyBuf, headers); bodyBuf = this.decompressBody(bodyBuf, headers);
} }
// Mark that we've seen an encrypted response // Mark that we've seen an encrypted response
if (headers['wtv-encrypted'] === 'true') { if (headers['wtv-encrypted'] === 'true') {
this.hasSeenEncryptedResponse = true; this.hasSeenEncryptedResponse = true;
@@ -968,7 +1118,7 @@ class WebTVClientSimulator {
// Set incarnation from server if provided // Set incarnation from server if provided
if (headers["wtv-incarnation"]) { if (headers["wtv-incarnation"]) {
this.debugLog(`Setting incarnation from server: ${headers["wtv-incarnation"]}`); this.debugLog(`Setting incarnation from server: ${headers["wtv-incarnation"]} (previous incarnation: ${this.incarnation})`);
this.wtvsec.set_incarnation(parseInt(headers["wtv-incarnation"])); this.wtvsec.set_incarnation(parseInt(headers["wtv-incarnation"]));
this.incarnation = parseInt(headers["wtv-incarnation"]); this.incarnation = parseInt(headers["wtv-incarnation"]);
} }
@@ -977,14 +1127,13 @@ class WebTVClientSimulator {
// For subsequent challenges (like during user login), use the current shared key // For subsequent challenges (like during user login), use the current shared key
// For the first challenge, use the initial key if provided // For the first challenge, use the initial key if provided
let keyToUse = this.wtvsec.current_shared_key || this.initial_key let keyToUse = this.wtvsec.current_shared_key || this.initial_key
this.debugLog(`Using key for challenge: ${keyToUse.toString(CryptoJS.enc.Base64)}`); this.debugLog(`Using key for challenge: ${keyToUse.toString(CryptoJS.enc.Base64)}`);
this.wtvsec.set_incarnation(this.incarnation); this.wtvsec.set_incarnation(this.incarnation);
this.debugLog(`Using incarnation for challenge: ${this.wtvsec.incarnation}`); this.debugLog(`Using incarnation for challenge: ${this.wtvsec.incarnation}`);
const challengeResponse = this.wtvsec.ProcessChallenge(headers['wtv-challenge'], keyToUse); const challengeResponse = this.wtvsec.ProcessChallenge(headers['wtv-challenge'], keyToUse);
if (challengeResponse && challengeResponse.toString(CryptoJS.enc.Base64)) { if (challengeResponse && challengeResponse.toString(CryptoJS.enc.Base64)) {
this.initial_key = this.wtvsec.initial_shared_key.toString(CryptoJS.enc.Base64);
this.debugLog('Challenge processed successfully, preparing response'); this.debugLog('Challenge processed successfully, preparing response');
this.debugLog(`Challenge response: ${challengeResponse.toString(CryptoJS.enc.Base64)}`); this.debugLog(`Challenge response: ${challengeResponse.toString(CryptoJS.enc.Base64)}`);
// We'll send the challenge response in the next request // We'll send the challenge response in the next request
@@ -997,43 +1146,6 @@ class WebTVClientSimulator {
this.wtvsec.SecureOn(); // Initialize RC4 sessions this.wtvsec.SecureOn(); // Initialize RC4 sessions
// Note: this.encryptionEnabled will be set to true only after authentication // Note: this.encryptionEnabled will be set to true only after authentication
} }
} else {
console.error('Failed to process challenge - no response generated');
this.debugLog('Challenge processing failed, attempting with different key...');
// Try with a different key if the first attempt failed
try {
let alternativeKey;
if (this.challengeResponse && this.initial_key) {
// Try with initial key if we used current shared key
alternativeKey = CryptoJS.enc.Base64.parse(this.initial_key);
this.debugLog('Retrying challenge with initial key');
} else if (!this.challengeResponse && this.wtvsec.current_shared_key) {
// Try with current shared key if we used initial key
alternativeKey = this.wtvsec.current_shared_key;
this.debugLog('Retrying challenge with current shared key');
}
if (alternativeKey) {
this.debugLog(`Retry key: ${alternativeKey.toString(CryptoJS.enc.Base64)}`);
const retryResponse = this.wtvsec.ProcessChallenge(headers['wtv-challenge'], alternativeKey);
if (retryResponse && retryResponse.toString(CryptoJS.enc.Base64)) {
this.debugLog('Challenge retry successful!');
this.challengeResponse = retryResponse.toString(CryptoJS.enc.Base64);
this.debugLog('Setting wtv-challenge-response header for next request');
// Enable encryption preparation if requested
if (this.useEncryption) {
this.debugLog('*** Encryption requested - preparing encryption after challenge retry ***');
this.wtvsec.SecureOn(); // Initialize RC4 sessions
}
} else {
console.error('Challenge retry also failed');
}
}
} catch (retryError) {
console.error('Challenge retry failed:', retryError.message);
}
} }
} catch (error) { } catch (error) {
console.error('Error processing challenge:', error.message); console.error('Error processing challenge:', error.message);
@@ -2116,6 +2228,184 @@ class WebTVClientSimulator {
return `content_${hash}${ext}`; return `content_${hash}${ext}`;
} }
parseVLNStageTwoHTML(htmlContent) {
const userList = [];
try {
const htmlString = Buffer.isBuffer(htmlContent) ? htmlContent.toString('utf8') : htmlContent;
this.debugLog('Parsing VLN-stage-two HTML for form and hidden fields...');
// Find the form action URL
var formMatch = htmlString.match(/<form[^>]+action=["']([^"']+)["'][^>]*>/i);
if (!formMatch) {
// Match <form ... action=... ...>
// Handles quoted and unquoted action values
// Improved regex: match action attribute, quoted or unquoted, non-greedy
var formMatch = htmlString.match(/<form[^>]*\saction\s*=\s*(?:"([^"]*?)"|'([^']*?)'|([^\s"'<>]+))/i);
if (!formMatch) {
console.error('No form with action found in VLN-stage-two HTML');
this.cleanup();
process.exit(1);
}
}
// Pick the first non-null group (quoted or unquoted)
let formAction = formMatch[1] || formMatch[2] || formMatch[3];
if (formAction.startsWith('/')) {
formAction = 'wtv-head-waiter:' + formAction;
}
// Extract all hidden input fields
const hiddenInputs = {};
// Regex to match hidden input fields, with or without quotes around attributes
const hiddenRegex = /<input[^>]+type=(?:"|')?hidden(?:"|')?[^>]*name=(?:"|')?([^"'\s>]+)(?:"|')?[^>]*value=(?:"|')?([^"'\s>]*)/gi;
let match;
while ((match = hiddenRegex.exec(htmlString)) !== null) {
hiddenInputs[match[1]] = match[2];
this.debugLog(`Found hidden input: ${match[1]} = ${match[2]}`);
}
// Require password from CLI
const passwordArgIndex = process.argv.indexOf('--password');
let password = null;
if (passwordArgIndex !== -1 && process.argv.length > passwordArgIndex + 1) {
password = process.argv[passwordArgIndex + 1];
}
if (!password) {
console.error('\nVLN-stage-two requires a password. Please provide one with --password <password>');
this.cleanup();
process.exit(1);
}
if (password) {
// Return early to prevent further processing
return {
formAction,
formData: { ...hiddenInputs, password },
userList: [],
selectedUser: null
};
}
} catch (error) {
console.error('Error parsing VLN-stage-two HTML:', error);
return [];
}
}
parseMinisrvChooseUser(htmlContent) {
const userList = [];
try {
const htmlString = Buffer.isBuffer(htmlContent) ? htmlContent.toString('utf8') : htmlContent;
this.debugLog('Parsing choose-user HTML for usernames and links...');
// Pattern to match user entries in the HTML
// Looking for <a href="/ValidateLogin?..." followed by username in <font> tags
// Handle both: <font><b>username</b></font> and <font>username</font>
// Examples:
// <a href=/ValidateLogin?user_id=0&user_login=true nocancel><font size=+1><b>minisrv_15413</b></font></a>
// <a href=/ValidateLogin?user_id=1&user_login=true nocancel><font size=+1>zefie2</font>
const userPattern = /<a\s+href=([^\s>]+)[^>]*>[\s\S]*?<font[^>]*>\s*(?:<b>)?([^<]+?)(?:<\/b>)?\s*<\/font>/gi;
let match;
while ((match = userPattern.exec(htmlString)) !== null) {
let href = match[1];
// Remove quotes if present
href = href.replace(/^["']|["']$/g, '');
// Add protocol if relative path
if (href.slice(0,1) === "/") {
href = "wtv-head-waiter:" + href;
}
const username = match[2].trim();
if (username && href) {
userList.push({
username: username,
href: href
});
this.debugLog(`Found user: ${username} -> ${href}`);
}
}
// Alternative pattern in case the first one doesn't catch all cases
// Look for any ValidateLogin links and try to find nearby usernames
if (userList.length === 0) {
this.debugLog('Primary pattern found no users, trying alternative pattern...');
const linkPattern = /href="?([^"\s]*ValidateLogin[^"\s]*)"?/gi;
const fontPattern = /<font[^>]*>(?:<b>)?([^<]+?)(?:<\/b>)?<\/font>/gi;
const links = [];
const usernames = [];
let linkMatch;
while ((linkMatch = linkPattern.exec(htmlString)) !== null) {
let href = linkMatch[1];
// Remove quotes if present
href = href.replace(/^["']|["']$/g, '');
// Add protocol if relative path
if (href.slice(0,1) === "/") {
href = "wtv-head-waiter:" + href;
}
links.push(href);
}
let fontMatch;
while ((fontMatch = fontPattern.exec(htmlString)) !== null) {
usernames.push(fontMatch[1].trim());
}
// Try to pair them up (assuming they appear in the same order)
const minLength = Math.min(links.length, usernames.length);
for (let i = 0; i < minLength; i++) {
userList.push({
username: usernames[i],
href: links[i]
});
this.debugLog(`Found user (alternative): ${usernames[i]} -> ${links[i]}`);
}
}
this.debugLog(`Parsed ${userList.length} users from choose-user HTML`);
if (this.username) {
// Find the specified user
const selectedUser = userList.find(user => user.username.toLowerCase() === this.username.toLowerCase());
if (selectedUser) {
this.debugLog(`*** Selecting user: ${selectedUser.username} ***`);
console.log(`Selecting user: ${selectedUser.username}`);
// Return the user list with the selected user marked for automatic following
return { userList, selectedUser };
} else {
console.error(`\nUser '${this.username}' not found. Available users: ${userList.map(user => user.username).join(', ')}`);
this.cleanup();
process.exit(1);
}
} else {
// No username specified, list available users and exit
if (userList.length > 0) {
console.log('\n*** Available users from choose-user ***');
userList.forEach((user, index) => {
console.log(`${index + 1}. ${user.username} -> ${user.href}`);
});
console.log('*** End of user list ***\n');
console.error(`Please select a --user: ${userList.map(user => user.username).join(' ')}`);
this.cleanup();
process.exit(1);
} else {
console.error('No users found in choose-user HTML');
this.cleanup();
process.exit(1);
}
}
return { userList };
} catch (error) {
console.error('Error parsing choose-user HTML:', error);
return [];
}
}
/** /**
* Parse login-stage-two HTML to extract usernames and their respective href links * Parse login-stage-two HTML to extract usernames and their respective href links
*/ */