307 lines
9.9 KiB
JavaScript
Executable File
307 lines
9.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// aether-browser: Playwright helper for Aether web UI automation
|
|
// Usage: aether-browser <action> [args...]
|
|
//
|
|
// Actions:
|
|
// login - Login and save session cookies
|
|
// screenshot <path> - Take screenshot of current page
|
|
// haproxy-status - Check HAProxy infrastructure status
|
|
// haproxy-images - List HAProxy images
|
|
// navigate <url> - Navigate to URL and return page content
|
|
// eval <js> - Evaluate JavaScript on current page
|
|
//
|
|
// Environment:
|
|
// AETHER_URL - Aether base URL (default: https://192.168.102.160:8443)
|
|
// AETHER_ADMIN_PASSWORD - Admin password (required)
|
|
// AETHER_COOKIE_FILE - Cookie persistence file (default: /tmp/aether-cookies.json)
|
|
// HEADLESS - Set to "false" for visible browser (default: true)
|
|
|
|
const { chromium } = require('playwright-core');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const AETHER_URL = process.env.AETHER_URL || 'https://192.168.102.160:8443';
|
|
const PASSWORD = process.env.AETHER_ADMIN_PASSWORD;
|
|
const COOKIE_FILE = process.env.AETHER_COOKIE_FILE || '/tmp/aether-cookies.json';
|
|
const HEADLESS = process.env.HEADLESS !== 'false';
|
|
const SCREENSHOT_DIR = '/tmp';
|
|
|
|
if (!PASSWORD) {
|
|
console.error('ERROR: AETHER_ADMIN_PASSWORD not set. Source the env file first.');
|
|
process.exit(1);
|
|
}
|
|
|
|
async function getBrowser() {
|
|
// Use Playwright's bundled Chromium
|
|
const browserPath = require('playwright-core').chromium.executablePath();
|
|
return await chromium.launch({
|
|
headless: HEADLESS,
|
|
executablePath: browserPath,
|
|
args: ['--ignore-certificate-errors', '--no-sandbox'],
|
|
});
|
|
}
|
|
|
|
async function getContextWithCookies(browser) {
|
|
const context = await browser.newContext({
|
|
ignoreHTTPSErrors: true,
|
|
viewport: { width: 1280, height: 800 },
|
|
});
|
|
|
|
// Restore cookies if available
|
|
if (fs.existsSync(COOKIE_FILE)) {
|
|
try {
|
|
const cookies = JSON.parse(fs.readFileSync(COOKIE_FILE, 'utf8'));
|
|
await context.addCookies(cookies);
|
|
} catch (e) {
|
|
// Stale cookies, will re-login
|
|
}
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
async function saveCookies(context) {
|
|
const cookies = await context.cookies();
|
|
fs.writeFileSync(COOKIE_FILE, JSON.stringify(cookies, null, 2));
|
|
}
|
|
|
|
async function login(page) {
|
|
console.log(`Navigating to ${AETHER_URL}/...`);
|
|
await page.goto(`${AETHER_URL}/`, { waitUntil: 'networkidle' });
|
|
|
|
// Check if already logged in
|
|
const url = page.url();
|
|
if (!url.includes('login') && !await page.locator('input[name="csrf_token"]').count()) {
|
|
console.log('Already logged in (session cookies valid).');
|
|
return true;
|
|
}
|
|
|
|
console.log('Login page detected. Filling credentials...');
|
|
|
|
// Fill the login form
|
|
await page.fill('input[name="username"]', 'admin');
|
|
await page.fill('input[name="password"]', PASSWORD);
|
|
|
|
// Submit
|
|
await page.click('button[type="submit"], input[type="submit"]');
|
|
|
|
// Wait for navigation after login
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Check success
|
|
const afterUrl = page.url();
|
|
if (afterUrl.includes('login')) {
|
|
console.error('ERROR: Login failed - still on login page.');
|
|
await page.screenshot({ path: `${SCREENSHOT_DIR}/aether-login-fail.png` });
|
|
console.error(`Screenshot saved to ${SCREENSHOT_DIR}/aether-login-fail.png`);
|
|
return false;
|
|
}
|
|
|
|
console.log(`Login successful. Redirected to: ${afterUrl}`);
|
|
return true;
|
|
}
|
|
|
|
async function doAction(action, args) {
|
|
const browser = await getBrowser();
|
|
const context = await getContextWithCookies(browser);
|
|
const page = await context.newPage();
|
|
|
|
try {
|
|
switch (action) {
|
|
case 'login': {
|
|
const success = await login(page);
|
|
if (success) {
|
|
await saveCookies(context);
|
|
console.log(`Session cookies saved to ${COOKIE_FILE}`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'screenshot': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
const targetUrl = args[0] || '/';
|
|
if (targetUrl.startsWith('/')) {
|
|
await page.goto(`${AETHER_URL}${targetUrl}`, { waitUntil: 'networkidle' });
|
|
}
|
|
|
|
const outPath = args[1] || `${SCREENSHOT_DIR}/aether-screenshot.png`;
|
|
await page.screenshot({ path: outPath, fullPage: true });
|
|
console.log(`Screenshot saved to ${outPath}`);
|
|
break;
|
|
}
|
|
|
|
case 'haproxy-status': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
// Navigate to HAProxy infrastructure page
|
|
console.log('Navigating to HAProxy management...');
|
|
await page.goto(`${AETHER_URL}/haproxy`, { waitUntil: 'networkidle' });
|
|
await page.screenshot({ path: `${SCREENSHOT_DIR}/aether-haproxy.png`, fullPage: true });
|
|
console.log(`Screenshot: ${SCREENSHOT_DIR}/aether-haproxy.png`);
|
|
|
|
// Try to extract status info from the page
|
|
const content = await page.textContent('body');
|
|
// Print relevant sections
|
|
const lines = content.split('\n').filter(l => l.trim());
|
|
console.log('\n--- Page content (filtered) ---');
|
|
for (const line of lines.slice(0, 50)) {
|
|
console.log(line.trim());
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'haproxy-images': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
console.log('Fetching HAProxy images...');
|
|
// Use the session to make an API call
|
|
const response = await page.evaluate(async () => {
|
|
const resp = await fetch('/haproxy/images', {
|
|
credentials: 'same-origin',
|
|
});
|
|
return { status: resp.status, body: await resp.text() };
|
|
});
|
|
console.log(`Status: ${response.status}`);
|
|
console.log(response.body);
|
|
break;
|
|
}
|
|
|
|
case 'navigate': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
const navUrl = args[0] || '/';
|
|
const fullUrl = navUrl.startsWith('http') ? navUrl : `${AETHER_URL}${navUrl}`;
|
|
console.log(`Navigating to ${fullUrl}...`);
|
|
await page.goto(fullUrl, { waitUntil: 'networkidle' });
|
|
|
|
const outPath = `${SCREENSHOT_DIR}/aether-navigate.png`;
|
|
await page.screenshot({ path: outPath, fullPage: true });
|
|
console.log(`Screenshot: ${outPath}`);
|
|
|
|
const title = await page.title();
|
|
console.log(`Title: ${title}`);
|
|
console.log(`URL: ${page.url()}`);
|
|
break;
|
|
}
|
|
|
|
case 'api-get': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
const endpoint = args[0];
|
|
if (!endpoint) {
|
|
console.error('Usage: aether-browser api-get <endpoint>');
|
|
break;
|
|
}
|
|
|
|
const response = await page.evaluate(async (url) => {
|
|
const resp = await fetch(url, { credentials: 'same-origin' });
|
|
const text = await resp.text();
|
|
return { status: resp.status, contentType: resp.headers.get('content-type'), body: text };
|
|
}, endpoint);
|
|
|
|
console.log(`GET ${endpoint} -> ${response.status}`);
|
|
if (response.contentType && response.contentType.includes('json')) {
|
|
try {
|
|
console.log(JSON.stringify(JSON.parse(response.body), null, 2));
|
|
} catch {
|
|
console.log(response.body);
|
|
}
|
|
} else {
|
|
console.log(response.body);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'api-post': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
const endpoint = args[0];
|
|
const bodyJson = args[1];
|
|
if (!endpoint || !bodyJson) {
|
|
console.error('Usage: aether-browser api-post <endpoint> <json-body>');
|
|
break;
|
|
}
|
|
|
|
// Get CSRF token from meta tag or cookie
|
|
const csrfToken = await page.evaluate(() => {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
if (meta) return meta.getAttribute('content');
|
|
// Try from cookie
|
|
const match = document.cookie.match(/csrf_token=([^;]+)/);
|
|
return match ? match[1] : '';
|
|
});
|
|
|
|
const response = await page.evaluate(async ({ url, body, csrf }) => {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
if (csrf) {
|
|
headers['X-CSRFToken'] = csrf;
|
|
headers['X-CSRF-Token'] = csrf;
|
|
}
|
|
const resp = await fetch(url, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers,
|
|
body,
|
|
});
|
|
const text = await resp.text();
|
|
return { status: resp.status, body: text };
|
|
}, { url: endpoint, body: bodyJson, csrf: csrfToken });
|
|
|
|
console.log(`POST ${endpoint} -> ${response.status}`);
|
|
try {
|
|
console.log(JSON.stringify(JSON.parse(response.body), null, 2));
|
|
} catch {
|
|
console.log(response.body);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'eval': {
|
|
const loginOk = await login(page);
|
|
if (!loginOk) break;
|
|
await saveCookies(context);
|
|
|
|
const js = args.join(' ');
|
|
const result = await page.evaluate(js);
|
|
console.log(JSON.stringify(result, null, 2));
|
|
break;
|
|
}
|
|
|
|
default:
|
|
console.error(`Unknown action: ${action}`);
|
|
console.error('Actions: login, screenshot, haproxy-status, haproxy-images, navigate, api-get, api-post, eval');
|
|
process.exit(1);
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
// Main
|
|
const [action, ...args] = process.argv.slice(2);
|
|
if (!action) {
|
|
console.error('Usage: aether-browser <action> [args...]');
|
|
console.error('Actions: login, screenshot, haproxy-status, haproxy-images, navigate, api-get, api-post, eval');
|
|
process.exit(1);
|
|
}
|
|
|
|
doAction(action, args).catch(err => {
|
|
console.error('Fatal error:', err.message);
|
|
process.exit(1);
|
|
});
|