#!/usr/bin/env node // aether-browser: Playwright helper for Aether web UI automation // Usage: aether-browser [args...] // // Actions: // login - Login and save session cookies // screenshot - Take screenshot of current page // haproxy-status - Check HAProxy infrastructure status // haproxy-images - List HAProxy images // navigate - Navigate to URL and return page content // eval - 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 '); 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 '); 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 [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); });