532 lines
20 KiB
JavaScript
532 lines
20 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const toml = require('@iarna/toml');
|
|
const puppeteer = require('puppeteer-extra');
|
|
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
|
puppeteer.use(StealthPlugin());
|
|
|
|
const FIELD_PATTERNS = {
|
|
email: {
|
|
priority: 1,
|
|
types: ['email'],
|
|
names: ['email', 'e-mail', 'mail', 'user_email', 'login_email', 'email_address', 'emailaddress'],
|
|
ids: ['email', 'e-mail', 'mail', 'user-email', 'login-email'],
|
|
placeholders: ['email', 'e-mail', 'email address', 'your email', 'enter email'],
|
|
autocomplete: ['email']
|
|
},
|
|
confirmEmail: {
|
|
priority: 2,
|
|
types: ['email'],
|
|
names: ['confirm_email', 'confirmemail', 'email_confirm', 'verify_email', 'email_verify'],
|
|
ids: ['confirm-email', 'confirmemail', 'verify-email', 'email-confirm'],
|
|
placeholders: ['confirm email', 'verify email', 're-enter email', 'repeat email'],
|
|
mustHaveConfirmIndicator: true
|
|
},
|
|
firstName: {
|
|
priority: 3,
|
|
types: ['text'],
|
|
names: ['first_name', 'firstname', 'fname', 'first-name', 'vorname', 'prenom', 'fname'],
|
|
ids: ['first-name', 'firstname', 'fname', 'firstName', 'mce-fname'],
|
|
placeholders: ['first name', 'firstname', 'first'],
|
|
autocomplete: ['given-name', 'first-name']
|
|
},
|
|
lastName: {
|
|
priority: 4,
|
|
types: ['text'],
|
|
names: ['last_name', 'lastname', 'lname', 'surname', 'family_name', 'last-name', 'lname'],
|
|
ids: ['last-name', 'lastname', 'lname', 'surname', 'lastName', 'mce-lname'],
|
|
placeholders: ['last name', 'lastname', 'surname', 'last'],
|
|
autocomplete: ['family-name', 'last-name']
|
|
},
|
|
username: {
|
|
priority: 5,
|
|
types: ['text'],
|
|
names: ['username', 'user_name', 'login', 'usr', 'uname', 'user-id', 'userid', 'account_name', 'login_id'],
|
|
ids: ['username', 'user-name', 'login', 'usr', 'uname'],
|
|
placeholders: ['username', 'user name', 'login', 'username or email'],
|
|
autocomplete: ['username']
|
|
},
|
|
password: {
|
|
priority: 6,
|
|
types: ['password'],
|
|
names: ['password', 'pass', 'pwd', 'user_password', 'login_password'],
|
|
ids: ['password', 'pass', 'pwd'],
|
|
placeholders: ['password', 'pass', 'enter password'],
|
|
autocomplete: ['current-password', 'new-password']
|
|
},
|
|
fullName: {
|
|
priority: 7,
|
|
types: ['text'],
|
|
names: ['full_name', 'fullname', 'name'],
|
|
ids: ['full-name', 'fullname', 'name'],
|
|
placeholders: ['full name', 'your name', 'complete name'],
|
|
autocomplete: ['name']
|
|
},
|
|
phone: {
|
|
priority: 8,
|
|
types: ['tel', 'text'],
|
|
names: ['phone', 'telephone', 'mobile', 'cell', 'cellphone', 'phone_number'],
|
|
ids: ['phone', 'telephone', 'mobile', 'cell', 'tel'],
|
|
placeholders: ['phone', 'telephone', 'mobile number'],
|
|
autocomplete: ['tel']
|
|
},
|
|
age: {
|
|
priority: 9,
|
|
types: ['number', 'text'],
|
|
names: ['age', 'user_age'],
|
|
ids: ['age'],
|
|
placeholders: ['age', 'your age']
|
|
},
|
|
company: {
|
|
priority: 10,
|
|
types: ['text'],
|
|
names: ['company', 'organization', 'company_name', 'employer'],
|
|
ids: ['company', 'organization', 'employer'],
|
|
placeholders: ['company', 'organization'],
|
|
autocomplete: ['organization']
|
|
}
|
|
};
|
|
|
|
function loadConfig() {
|
|
try {
|
|
const configPath = path.join(__dirname, 'config.toml');
|
|
const tomlString = fs.readFileSync(configPath, 'utf-8');
|
|
const config = toml.parse(tomlString);
|
|
console.log('✅ Configuration loaded successfully.');
|
|
return config;
|
|
} catch (error) {
|
|
console.error('❌ Failed to load or parse config.toml:', error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function generateRandomData() {
|
|
const randomString = Math.random().toString(36).substring(2, 10);
|
|
const username = `user${randomString}`;
|
|
const email = `${username}@gmail.com`;
|
|
const password = `pass${randomString}`;
|
|
const age = Math.floor(Math.random() * 30 + 20).toString();
|
|
const firstName = 'John';
|
|
const lastName = 'Doe';
|
|
const phone = '+31610488717';
|
|
const company = 'Example Corp';
|
|
return { username, email, password, age, firstName, lastName, phone, company };
|
|
}
|
|
|
|
function parseLocator(locatorStr) {
|
|
locatorStr = locatorStr.trim();
|
|
if (!locatorStr.startsWith('<')) {
|
|
return locatorStr;
|
|
}
|
|
|
|
const getAttr = (attr) => {
|
|
const match = locatorStr.match(new RegExp(`${attr}=["']([^"']+)["']`));
|
|
return match ? match[1] : null;
|
|
};
|
|
|
|
const tagName = (locatorStr.match(/^<([a-zA-Z0-9]+)/) || [])[1] || '';
|
|
const id = getAttr('id');
|
|
if (id) {
|
|
if (/^\d/.test(id)) {
|
|
return `${tagName}[id='${id}']`;
|
|
}
|
|
return `${tagName}#${id}`;
|
|
}
|
|
|
|
const name = getAttr('name');
|
|
if (name) return `${tagName}[name='${name}']`;
|
|
|
|
const placeholder = getAttr('placeholder');
|
|
if (placeholder) return `${tagName}[placeholder='${placeholder}']`;
|
|
|
|
const type = getAttr('type');
|
|
if (type) return `${tagName}[type='${type}']`;
|
|
|
|
return tagName;
|
|
}
|
|
|
|
async function detectFormFields(page) {
|
|
console.log('🔍 Auto-detecting form fields...');
|
|
|
|
const detectedFields = await page.evaluate((patterns) => {
|
|
const results = {};
|
|
const usedElements = new Set();
|
|
|
|
// Get all visible input fields, excluding hidden fields
|
|
const inputs = Array.from(document.querySelectorAll('input, select, textarea')).filter(input => {
|
|
const type = input.type?.toLowerCase() || 'text';
|
|
const style = window.getComputedStyle(input);
|
|
const rect = input.getBoundingClientRect();
|
|
|
|
// Skip hidden fields and honeypots
|
|
if (type === 'hidden' || type === 'submit' || type === 'button') return false;
|
|
if (input.tabIndex === -1 && input.style.position === 'absolute') return false; // Likely honeypot
|
|
if (rect.width === 0 || rect.height === 0) return false;
|
|
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
const calculateScore = (input, pattern, fieldName) => {
|
|
let score = 0;
|
|
const inputType = (input.type || 'text').toLowerCase();
|
|
const inputName = (input.name || '').toLowerCase();
|
|
const inputId = (input.id || '').toLowerCase();
|
|
const placeholder = (input.placeholder || '').toLowerCase();
|
|
const autocomplete = (input.autocomplete || '').toLowerCase();
|
|
|
|
// Type matching
|
|
if (pattern.types && pattern.types.includes(inputType)) {
|
|
score += 5;
|
|
}
|
|
|
|
// Name matching - exact matches get high scores
|
|
if (pattern.names) {
|
|
for (const name of pattern.names) {
|
|
const nameLower = name.toLowerCase();
|
|
if (inputName === nameLower) {
|
|
score += 20; // Exact match
|
|
break;
|
|
} else if (inputName.includes(nameLower)) {
|
|
// Partial match - be careful not to match fname with username
|
|
if (fieldName === 'firstName' && (inputName.includes('user') || inputName.includes('login'))) {
|
|
score -= 5; // Penalize if it looks like username
|
|
} else if (fieldName === 'lastName' && (inputName.includes('user') || inputName.includes('login'))) {
|
|
score -= 5;
|
|
} else {
|
|
score += 10;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ID matching - very specific
|
|
if (pattern.ids) {
|
|
for (const id of pattern.ids) {
|
|
const idLower = id.toLowerCase();
|
|
if (inputId === idLower) {
|
|
score += 20; // Exact match
|
|
break;
|
|
} else if (inputId.includes(idLower)) {
|
|
score += 10;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Placeholder matching
|
|
if (pattern.placeholders) {
|
|
for (const ph of pattern.placeholders) {
|
|
const phLower = ph.toLowerCase();
|
|
if (placeholder === phLower) {
|
|
score += 15;
|
|
break;
|
|
} else if (placeholder.includes(phLower)) {
|
|
score += 8;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Autocomplete matching
|
|
if (pattern.autocomplete) {
|
|
for (const ac of pattern.autocomplete) {
|
|
if (autocomplete === ac.toLowerCase()) {
|
|
score += 12;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check label text
|
|
const labels = input.labels;
|
|
if (labels && labels.length > 0) {
|
|
const labelText = labels[0].textContent.toLowerCase();
|
|
const fieldNameLower = fieldName.toLowerCase().replace(/([A-Z])/g, ' $1').trim();
|
|
if (labelText.includes(fieldNameLower) || labelText.includes(pattern.names?.[0] || '')) {
|
|
score += 15;
|
|
}
|
|
}
|
|
|
|
// Penalize if field appears to be a honeypot
|
|
if (inputName.includes('b_') || inputId.includes('b_')) {
|
|
score -= 30; // Likely Mailchimp honeypot
|
|
}
|
|
|
|
// Penalize disabled/readonly
|
|
if (input.disabled || input.readOnly) {
|
|
score -= 20;
|
|
}
|
|
|
|
// Special handling for confirmEmail
|
|
if (pattern.mustHaveConfirmIndicator) {
|
|
const hasConfirm = inputId.includes('confirm') || inputId.includes('verify') ||
|
|
inputName.includes('confirm') || inputName.includes('verify') ||
|
|
placeholder.includes('confirm') || placeholder.includes('verify') ||
|
|
placeholder.includes('re-') || placeholder.includes('again');
|
|
if (!hasConfirm) {
|
|
score -= 15;
|
|
}
|
|
}
|
|
|
|
return score;
|
|
};
|
|
|
|
// Sort patterns by priority
|
|
const sortedPatterns = Object.entries(patterns).sort((a, b) => a[1].priority - b[1].priority);
|
|
|
|
for (const [fieldName, pattern] of sortedPatterns) {
|
|
let bestMatch = null;
|
|
let bestScore = 0;
|
|
let bestIndex = -1;
|
|
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
if (usedElements.has(i)) continue;
|
|
|
|
const input = inputs[i];
|
|
const score = calculateScore(input, pattern, fieldName);
|
|
|
|
if (score > bestScore && score >= 10) {
|
|
bestScore = score;
|
|
bestMatch = input;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
|
|
if (bestMatch && bestIndex >= 0) {
|
|
usedElements.add(bestIndex);
|
|
|
|
// Generate selector
|
|
let selector = '';
|
|
if (bestMatch.id) {
|
|
selector = `#${bestMatch.id}`;
|
|
} else if (bestMatch.name) {
|
|
selector = `[name="${bestMatch.name}"]`;
|
|
} else if (bestMatch.placeholder) {
|
|
selector = `[placeholder="${bestMatch.placeholder}"]`;
|
|
} else {
|
|
const tagName = bestMatch.tagName.toLowerCase();
|
|
const type = bestMatch.type ? `[type="${bestMatch.type}"]` : '';
|
|
const allSame = Array.from(document.querySelectorAll(`${tagName}${type}`));
|
|
const index = allSame.indexOf(bestMatch) + 1;
|
|
selector = `${tagName}${type}:nth-of-type(${index})`;
|
|
}
|
|
|
|
results[fieldName] = {
|
|
selector,
|
|
confidence: bestScore,
|
|
name: bestMatch.name,
|
|
id: bestMatch.id
|
|
};
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}, FIELD_PATTERNS);
|
|
|
|
const detectedCount = Object.keys(detectedFields).length;
|
|
if (detectedCount > 0) {
|
|
console.log(` ✅ Auto-detected ${detectedCount} fields:`);
|
|
for (const [field, info] of Object.entries(detectedFields)) {
|
|
console.log(` - ${field}: ${info.selector} (score: ${info.confidence})`);
|
|
}
|
|
} else {
|
|
console.log(' ⚠️ No fields auto-detected');
|
|
}
|
|
|
|
return detectedFields;
|
|
}
|
|
|
|
async function detectSubmitButton(page) {
|
|
console.log('🔍 Auto-detecting submit button...');
|
|
|
|
const submitSelector = await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"], button, input[type="button"]'));
|
|
let bestMatch = null;
|
|
let bestScore = 0;
|
|
|
|
buttons.forEach(btn => {
|
|
const text = (btn.textContent || btn.value || '').toLowerCase().trim();
|
|
const type = btn.type?.toLowerCase() || '';
|
|
const className = (btn.className || '').toLowerCase();
|
|
let score = 0;
|
|
|
|
if (type === 'submit') score += 20;
|
|
|
|
if (text.includes('subscribe')) score += 15;
|
|
else if (text.includes('sign up')) score += 12;
|
|
else if (text.includes('submit')) score += 10;
|
|
else if (text.includes('join')) score += 8;
|
|
else if (text.includes('register')) score += 8;
|
|
else if (text.includes('send')) score += 5;
|
|
|
|
if (className.includes('submit')) score += 8;
|
|
if (className.includes('subscribe')) score += 8;
|
|
|
|
if (className.includes('close') || text.includes('close')) score -= 20;
|
|
if (className.includes('cancel') || text.includes('cancel')) score -= 20;
|
|
if (className.includes('back') || text.includes('back')) score -= 15;
|
|
if (className.includes('drawer')) score -= 15;
|
|
|
|
const rect = btn.getBoundingClientRect();
|
|
if (rect.width > 0 && rect.height > 0 && rect.top > 0) {
|
|
score += 3;
|
|
}
|
|
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestMatch = btn;
|
|
}
|
|
});
|
|
|
|
if (bestMatch && bestScore >= 10) {
|
|
if (bestMatch.id) return `#${bestMatch.id}`;
|
|
if (bestMatch.name) return `[name="${bestMatch.name}"]`;
|
|
|
|
const className = bestMatch.className;
|
|
if (className) {
|
|
const classes = className.split(' ').filter(c => c.length > 0);
|
|
for (const cls of classes) {
|
|
const clsLower = cls.toLowerCase();
|
|
if (clsLower.includes('submit') || clsLower.includes('subscribe')) {
|
|
return `.${cls}`;
|
|
}
|
|
}
|
|
for (const cls of classes) {
|
|
if (!cls.toLowerCase().includes('close') && !cls.toLowerCase().includes('cancel')) {
|
|
return `.${cls}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const allBtns = Array.from(document.querySelectorAll('button'));
|
|
const index = allBtns.indexOf(bestMatch) + 1;
|
|
return `button:nth-of-type(${index})`;
|
|
}
|
|
|
|
return null;
|
|
});
|
|
|
|
if (submitSelector) {
|
|
console.log(` ✅ Auto-detected submit button: ${submitSelector}`);
|
|
}
|
|
|
|
return submitSelector;
|
|
}
|
|
|
|
async function main() {
|
|
const config = loadConfig();
|
|
const sitesToProcess = config.sites || [];
|
|
const browserConfig = config.browser || {};
|
|
const globalCreds = config.global_credentials || {};
|
|
const userDataDir = browserConfig.userDataDir;
|
|
|
|
if (!userDataDir) {
|
|
console.error('❌ userDataDir is not set in config.toml. This is required.');
|
|
return;
|
|
}
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: false,
|
|
userDataDir: userDataDir,
|
|
args: ["--start-maximized"]
|
|
});
|
|
|
|
try {
|
|
let isFirstSite = true;
|
|
for (const site of sitesToProcess) {
|
|
let creds;
|
|
if (site.credentials && site.credentials.username) {
|
|
creds = { ...globalCreds, ...site.credentials };
|
|
console.log(`Using credentials specified for site: ${site.name}`);
|
|
} else {
|
|
creds = { ...globalCreds };
|
|
console.log(`Using global credentials for site: ${site.name}`);
|
|
}
|
|
|
|
if (!creds.username || !creds.email || !creds.password) {
|
|
const randomData = generateRandomData();
|
|
creds = { ...randomData, ...creds };
|
|
console.log('Filled in missing credentials with random data.');
|
|
}
|
|
|
|
console.log(`\n--- Processing Site: ${site.name} (Category: ${site.category || 'N/A'}) ---`);
|
|
|
|
const page = isFirstSite ? (await browser.pages())[0] : await browser.newPage();
|
|
isFirstSite = false;
|
|
|
|
if (browserConfig.userAgent) {
|
|
await page.setUserAgent(browserConfig.userAgent);
|
|
}
|
|
|
|
await page.goto(site.url, { waitUntil: 'networkidle2' });
|
|
|
|
console.log('Dynamically filling out the form...');
|
|
|
|
let fieldsToFill = {};
|
|
const definedLocators = site.locators || {};
|
|
|
|
if (site.autoDetect !== false) {
|
|
const detectedFields = await detectFormFields(page);
|
|
for (const [fieldName, fieldInfo] of Object.entries(detectedFields)) {
|
|
fieldsToFill[fieldName] = fieldInfo.selector;
|
|
}
|
|
}
|
|
|
|
for (const [fieldName, locator] of Object.entries(definedLocators)) {
|
|
if (fieldName !== 'submit') {
|
|
fieldsToFill[fieldName] = parseLocator(locator);
|
|
}
|
|
}
|
|
|
|
for (const [fieldName, selector] of Object.entries(fieldsToFill)) {
|
|
let valueToFill = creds[fieldName];
|
|
|
|
if (fieldName === 'confirmEmail') {
|
|
valueToFill = creds['email'];
|
|
}
|
|
|
|
if (selector && valueToFill) {
|
|
try {
|
|
console.log(` - Filling field: ${fieldName} with "${valueToFill}"`);
|
|
await page.evaluate((sel, val) => {
|
|
const el = document.querySelector(sel);
|
|
if (el) {
|
|
el.value = val;
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
}, selector, valueToFill);
|
|
} catch (e) {
|
|
console.warn(` - Could not fill field '${fieldName}': ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
let submitSelector = null;
|
|
if (definedLocators.submit) {
|
|
submitSelector = parseLocator(definedLocators.submit);
|
|
} else if (site.autoDetect !== false) {
|
|
submitSelector = await detectSubmitButton(page);
|
|
}
|
|
|
|
if (submitSelector) {
|
|
console.log('Form filled. Attempting to submit...');
|
|
await page.click(submitSelector);
|
|
console.log(`✅ Submission attempt for ${site.name} is complete.`);
|
|
} else {
|
|
console.warn('⚠️ No submit button found.');
|
|
}
|
|
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
}
|
|
} catch(error) {
|
|
console.error('❌ A critical error occurred:', error.message);
|
|
} finally {
|
|
console.log('\n🎉 All tasks complete.');
|
|
}
|
|
}
|
|
|
|
main();
|