183 lines
7.4 KiB
JavaScript
183 lines
7.4 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* Request SSL certificates for the 7 NPMplus proxy hosts that have no cert.
|
|||
|
|
* Uses browser automation: for each host, edit → SSL → Request new certificate
|
|||
|
|
* → DNS Challenge → Cloudflare → (first credential) → email + agree → submit.
|
|||
|
|
*
|
|||
|
|
* Run from repo root: node scripts/nginx-proxy-manager/request-npmplus-7-certs-dns-ui.js
|
|||
|
|
* Requires: .env with NPM_URL, NPM_EMAIL, NPM_PASSWORD. HEADLESS=false to watch.
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { chromium } from 'playwright';
|
|||
|
|
import { fileURLToPath } from 'url';
|
|||
|
|
import { dirname, join } from 'path';
|
|||
|
|
import { config } from 'dotenv';
|
|||
|
|
import https from 'https';
|
|||
|
|
|
|||
|
|
const __filename = fileURLToPath(import.meta.url);
|
|||
|
|
const __dirname = dirname(__filename);
|
|||
|
|
const PROJECT_ROOT = join(__dirname, '../..');
|
|||
|
|
config({ path: join(PROJECT_ROOT, '.env') });
|
|||
|
|
|
|||
|
|
const NPM_URL = process.env.NPM_URL || 'https://192.168.11.167:81';
|
|||
|
|
const NPM_EMAIL = process.env.NPM_EMAIL || 'admin@example.org';
|
|||
|
|
const NPM_PASSWORD = process.env.NPM_PASSWORD;
|
|||
|
|
const LETSENCRYPT_EMAIL = process.env.SSL_EMAIL || process.env.NPM_EMAIL || NPM_EMAIL;
|
|||
|
|
const HEADLESS = process.env.HEADLESS !== 'false';
|
|||
|
|
const PAUSE_MODE = process.env.PAUSE_MODE === 'true';
|
|||
|
|
|
|||
|
|
// Host IDs for the 7 proxy hosts without a certificate (from list-npmplus-proxy-hosts-cert-status.sh)
|
|||
|
|
// Set FIRST_ONLY=1 to process only host 22 (for testing)
|
|||
|
|
const ALL_HOST_IDS = [22, 26, 24, 27, 28, 29, 25];
|
|||
|
|
const HOST_IDS_WITHOUT_CERT = process.env.FIRST_ONLY === '1' || process.env.FIRST_ONLY === 'true' ? ALL_HOST_IDS.slice(0, 1) : ALL_HOST_IDS;
|
|||
|
|
|
|||
|
|
if (!NPM_PASSWORD) {
|
|||
|
|
console.error('❌ NPM_PASSWORD is required. Set it in .env or export NPM_PASSWORD=...');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function log(msg, type = 'info') {
|
|||
|
|
const icons = { success: '✅', error: '❌', warning: '⚠️', info: '📋' };
|
|||
|
|
console.log(`${icons[type] || '📋'} ${msg}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function pause(page, message) {
|
|||
|
|
if (PAUSE_MODE) {
|
|||
|
|
log(`Paused: ${message}`, 'info');
|
|||
|
|
await page.pause();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function login(page) {
|
|||
|
|
log('Logging in to NPMplus...');
|
|||
|
|
await page.goto(NPM_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|||
|
|
await pause(page, 'At login page');
|
|||
|
|
|
|||
|
|
await page.waitForSelector('input[type="email"], input[name="email"], input[placeholder*="email" i]', { timeout: 10000 });
|
|||
|
|
const emailInput = await page.$('input[type="email"]') || await page.$('input[name="email"]') || await page.$('input[placeholder*="email" i]');
|
|||
|
|
if (emailInput) await emailInput.fill(NPM_EMAIL);
|
|||
|
|
const passwordInput = await page.$('input[type="password"]');
|
|||
|
|
if (passwordInput) await passwordInput.fill(NPM_PASSWORD);
|
|||
|
|
const loginButton = await page.$('button[type="submit"]') || await page.$('button:has-text("Sign In")') || await page.$('button:has-text("Login")');
|
|||
|
|
if (loginButton) await loginButton.click();
|
|||
|
|
else await page.keyboard.press('Enter');
|
|||
|
|
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
const url = page.url();
|
|||
|
|
if (url.includes('login') && !url.includes('proxy')) {
|
|||
|
|
const body = await page.textContent('body').catch(() => '');
|
|||
|
|
if (!body.includes('Proxy Hosts') && !body.includes('dashboard')) {
|
|||
|
|
log('Login may have failed – still on login page', 'warning');
|
|||
|
|
await page.screenshot({ path: join(PROJECT_ROOT, 'npmplus-login-check.png') }).catch(() => {});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
log('Logged in', 'success');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function requestCertForHostId(page, hostId) {
|
|||
|
|
log(`Requesting cert for host ID ${hostId}...`);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// NPM edit URL: open edit form directly by host ID
|
|||
|
|
await page.goto(`${NPM_URL}/#/proxy-hosts/edit/${hostId}`, { waitUntil: 'networkidle' });
|
|||
|
|
await page.waitForTimeout(3000);
|
|||
|
|
|
|||
|
|
// SSL tab: NPM edit form usually has Details | SSL | Advanced
|
|||
|
|
await page.getByText('SSL').first().click();
|
|||
|
|
await page.waitForTimeout(1500);
|
|||
|
|
|
|||
|
|
// "Request a new SSL Certificate" / "Get a new certificate"
|
|||
|
|
const requestBtn = page.getByRole('button', { name: /request.*(new )?ssl certificate|get.*certificate/i }).or(
|
|||
|
|
page.locator('button:has-text("Request"), button:has-text("Get a new"), a:has-text("Request")').first()
|
|||
|
|
);
|
|||
|
|
await requestBtn.click();
|
|||
|
|
await page.waitForTimeout(1500);
|
|||
|
|
|
|||
|
|
// DNS Challenge: click option/label for "DNS Challenge" or "Use a DNS Challenge"
|
|||
|
|
const dnsOption = page.getByText(/use a dns challenge|dns challenge/i).first();
|
|||
|
|
await dnsOption.click();
|
|||
|
|
await page.waitForTimeout(800);
|
|||
|
|
|
|||
|
|
// DNS Provider: Cloudflare (dropdown or first Cloudflare option)
|
|||
|
|
const cloudflareOption = page.getByText('Cloudflare').first();
|
|||
|
|
await cloudflareOption.click();
|
|||
|
|
await page.waitForTimeout(800);
|
|||
|
|
|
|||
|
|
// Credential: usually first in dropdown if only one Cloudflare credential
|
|||
|
|
const credSelect = page.locator('select').filter({ has: page.locator('option') }).first();
|
|||
|
|
if (await credSelect.count() > 0) {
|
|||
|
|
await credSelect.selectOption({ index: 1 });
|
|||
|
|
}
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
|
|||
|
|
// Email for Let's Encrypt
|
|||
|
|
const emailField = page.locator('input[type="email"], input[name*="email" i]').first();
|
|||
|
|
await emailField.fill(LETSENCRYPT_EMAIL);
|
|||
|
|
|
|||
|
|
// Agree to ToS
|
|||
|
|
const agree = page.locator('input[type="checkbox"]').filter({ has: page.locator('..') }).first();
|
|||
|
|
if (await agree.count() > 0 && !(await agree.isChecked())) await agree.check();
|
|||
|
|
|
|||
|
|
await pause(page, `Ready to submit cert request for host ${hostId}`);
|
|||
|
|
|
|||
|
|
// Submit
|
|||
|
|
const submitBtn = page.getByRole('button', { name: /save|submit|request|get certificate/i }).first();
|
|||
|
|
await submitBtn.click();
|
|||
|
|
await page.waitForTimeout(5000);
|
|||
|
|
|
|||
|
|
// Check for success or error
|
|||
|
|
const body = await page.textContent('body').catch(() => '');
|
|||
|
|
if (body.includes('error') && body.toLowerCase().includes('internal')) {
|
|||
|
|
log(`Request for host ${hostId} may have failed (Internal Error). Check NPM UI.`, 'warning');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
log(`Submitted cert request for host ${hostId}`, 'success');
|
|||
|
|
return true;
|
|||
|
|
} catch (e) {
|
|||
|
|
log(`Error for host ${hostId}: ${e.message}`, 'error');
|
|||
|
|
await page.screenshot({ path: join(PROJECT_ROOT, `npmplus-cert-error-${hostId}.png`) }).catch(() => {});
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|||
|
|
console.log('🔒 NPMplus – Request 7 certificates (DNS Cloudflare)');
|
|||
|
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|||
|
|
console.log('');
|
|||
|
|
log(`NPM: ${NPM_URL}`);
|
|||
|
|
log(`Host IDs: ${HOST_IDS_WITHOUT_CERT.join(', ')}`);
|
|||
|
|
console.log('');
|
|||
|
|
|
|||
|
|
const browser = await chromium.launch({ headless: HEADLESS, ignoreHTTPSErrors: true });
|
|||
|
|
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
|||
|
|
const page = await context.newPage();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const ok = await login(page);
|
|||
|
|
if (!ok) {
|
|||
|
|
log('Login failed', 'error');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let success = 0;
|
|||
|
|
for (const hostId of HOST_IDS_WITHOUT_CERT) {
|
|||
|
|
const ok = await requestCertForHostId(page, hostId);
|
|||
|
|
if (ok) success++;
|
|||
|
|
await page.waitForTimeout(2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('');
|
|||
|
|
log(`Done. Submitted requests for ${success}/${HOST_IDS_WITHOUT_CERT.length} hosts. Check NPM SSL Certificates and Hosts to confirm.`, 'success');
|
|||
|
|
log('Run: ./scripts/list-npmplus-proxy-hosts-cert-status.sh', 'info');
|
|||
|
|
} finally {
|
|||
|
|
await browser.close();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch((e) => {
|
|||
|
|
console.error(e);
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|