Uji subdomain
Uji subdomain
Section titled “Uji subdomain”DNS, jalur ACME, dan sertifikat, berurutan.
Berkas: alat/hari-3/Praktik 11/tools/uji-subdomain.mjs
#!/usr/bin/env node// =====================================================================// Praktik 11 bagian 3 - uji-subdomain.mjs//// Praktik 11 bagian 3 mengandalkan tiga hal yang sulit diperiksa secara// manual dan sering baru ketahuan gagal di tahap paling akhir://// - propagasi DNS subdomain// - kesiapan jalur ACME SEBELUM menjalankan certbot// - apakah sertifikat dan HTTPS benar-benar aktif SESUDAH-nya//// Skrip ini menjalankan pemeriksaan berurutan itu, dan berhenti lebih awal// kalau syarat berikutnya belum terpenuhi — supaya tidak membuang kuota// penerbitan sertifikat Let's Encrypt.//// Pakai:// node uji-subdomain.mjs nama01.gisbigtrainer.com// node uji-subdomain.mjs nama01.gisbigtrainer.com --ip 34.1.2.3// =====================================================================
import { Resolver } from 'node:dns/promises';import { connect } from 'node:tls';
const argv = process.argv.slice(2);const idxIp = argv.indexOf('--ip');const ipHarapan = idxIp > -1 ? argv[idxIp + 1] : null;// Buang nama opsi beserta nilainya; sisanya adalah argumen posisional.const buang = new Set();if (idxIp > -1) { buang.add(idxIp); buang.add(idxIp + 1); }const subdomain = argv.find((a, i) => !a.startsWith('--') && !buang.has(i));
if (!subdomain) { console.error('Pakai: node uji-subdomain.mjs <subdomain> [--ip <ip-statis-vm>]'); process.exit(2);}
const TIMEOUT = Number(process.env.TIMEOUT_MS ?? 10000);const hasil = [];const catat = (severity, judul, detail) => hasil.push({ severity, judul, detail });
async function ambil(url, opsi = {}) { const kontrol = new AbortController(); const timer = setTimeout(() => kontrol.abort(), TIMEOUT); try { return await fetch(url, { ...opsi, signal: kontrol.signal, redirect: 'manual' }); } finally { clearTimeout(timer); }}
console.log(`Subdomain : ${subdomain}`);if (ipHarapan) console.log(`IP harapan: ${ipHarapan}`);console.log('');
// ------------------------------------------------------------------ 1// Tahap 4: resolusi DNSlet ipResolusi = [];try { const resolver = new Resolver(); resolver.setServers(['1.1.1.1', '8.8.8.8']); ipResolusi = await resolver.resolve4(subdomain); if (ipResolusi.length === 0) ipResolusi = await resolver.resolveCname(subdomain);} catch (e) { catat('kritis', 'Subdomain belum bisa di-resolve oleh resolver publik', e.message);}
if (ipResolusi.length) { console.log(`1. DNS : ${ipResolusi.join(', ')}`); if (ipHarapan && !ipResolusi.includes(ipHarapan)) { catat('kritis', `DNS menunjuk ke ${ipResolusi.join(', ')}, seharusnya ${ipHarapan}`, 'Record A belum diperbarui, atau TTL lama masih tersimpan di cache resolver.'); } else if (ipHarapan) { console.log(' -> cocok dengan IP statis VM'); }} else if (hasil.every((h) => h.severity !== 'kritis')) { catat('kritis', 'Record A subdomain belum ada', 'Buat record A pada pengelola DNS, TTL 300, menunjuk ke IP statis VM.');}
// ------------------------------------------------------------------ 2// Tahap 5: HTTP lewat subdomainif (ipResolusi.length) { try { const r = await ambil(`http://${subdomain}/portal`); const lokasi = r.headers.get('location'); console.log(`2. HTTP : ${r.status}${lokasi ? ' -> ' + lokasi : ''}`); if (r.status === 0) { catat('tinggi', 'Tidak ada balasan HTTP lewat subdomain', 'Periksa firewall GCP: port 80 harus terbuka, dan container nginx harus jalan.'); } } catch (e) { catat('tinggi', 'Permintaan HTTP lewat subdomain gagal', e.message); }}
// ------------------------------------------------------------------ 3// Tahap 8: jalur ACME. INI YANG PALING PENTING DIPERIKSA SEBELUM CERTBOT.try { const uji = `http://${subdomain}/.well-known/acme-challenge/uji-coba`; const r = await ambil(uji); const badan = await r.text(); console.log(`3. ACME : ${r.status} (${badan.slice(0, 40).replace(/\n/g, ' ') || 'tanpa isi'})`);
if (r.status === 404) { // Yang kita inginkan: 404 dari Nginx karena berkas uji memang tidak ada. // Yang tidak kita inginkan: 404 dari halaman aplikasi, yang berarti // permintaan tidak pernah sampai ke webroot. // // Penanda aplikasi dicari secara spesifik. Sebelumnya penanda dibuat // terlalu longgar (<html saja) sehingga halaman 404 milik server apa pun // — termasuk Cloudflare — ikut dilaporkan sebagai salah konfigurasi. const penandaNext = /__NEXT_DATA__|\/_next\/static|__next_f/i.test(badan); const penandaNginx = /nginx/i.test(r.headers.get('server') ?? '') || badan.trim() === '';
if (penandaNext) { catat('kritis', 'Permintaan ACME diteruskan ke aplikasi Next.js, bukan ke webroot Nginx', 'Location /.well-known/acme-challenge/ belum ada di nginx.conf.\n' + 'Certbot akan gagal dengan "Invalid response ... 404".\n' + 'Tambahkan: location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }'); } else if (!penandaNginx) { catat('sedang', 'Balasan 404 pada jalur ACME tidak jelas berasal dari Nginx', 'Jalur ACME belum bisa dipastikan siap. Sebelum menjalankan certbot, pastikan\n' + 'nginx.conf memuat: location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }'); } else { console.log(' -> 404 dari Nginx, sesuai harapan. Certbot siap dijalankan.'); } } else if (r.status === 502 || r.status === 200) { catat('kritis', `Jalur ACME membalas ${r.status}, bukan 404`, 'Periksa konfigurasi location untuk ACME pada Nginx.'); }} catch (e) { catat('sedang', 'Tidak bisa memeriksa jalur ACME', e.message);}
// ------------------------------------------------------------------ 4// Tahap 11 & 14: HTTPStry { const r = await ambil(`https://${subdomain}/portal`); console.log(`4. HTTPS : ${r.status}`); if (r.status === 0) catat('sedang', 'HTTPS belum aktif', 'Normal bila sertifikat belum diterbitkan.');} catch (e) { catat('sedang', 'Permintaan HTTPS gagal', `${e.message}\nNormal bila sertifikat belum diterbitkan (Tahap 9-10 belum dijalankan).`);}
// ------------------------------------------------------------------ 5// Periksa sertifikat langsung di tingkat TLS: nama, masa berlaku, penerbit.try { const info = await new Promise((selesai, tolak) => { const soket = connect({ host: subdomain, port: 443, servername: subdomain, timeout: TIMEOUT }, () => { const sert = soket.getPeerCertificate(); const sah = soket.authorized; const alasan = soket.authorizationError; soket.end(); selesai({ sert, sah, alasan }); }); soket.once('error', tolak); soket.once('timeout', () => { soket.destroy(); tolak(new Error('timeout')); }); });
if (info.sert && Object.keys(info.sert).length) { const berlakuSampai = new Date(info.sert.valid_to); const sisaHari = Math.round((berlakuSampai - Date.now()) / 86400000); console.log(`5. Sertifikat: subjek ${info.sert.subject?.CN ?? '?'}, berlaku sampai ${berlakuSampai.toISOString().slice(0, 10)} (${sisaHari} hari lagi)`);
const cn = info.sert.subject?.CN ?? ''; const alt = info.sert.subjectaltname ?? ''; if (cn !== subdomain && !alt.includes(subdomain)) { catat('kritis', `Sertifikat tidak berlaku untuk ${subdomain}`, `CN=${cn}, SAN=${alt}. Certbot mungkin menerbitkan untuk domain yang berbeda.`); } if (sisaHari < 20) { catat('sedang', `Sertifikat kedaluwarsa dalam ${sisaHari} hari`, 'Periksa Tahap 12: perpanjangan otomatis dan hook reload Nginx.'); } if (!info.sah) { catat('tinggi', `Sertifikat tidak tepercaya: ${info.alasan}`, 'Bisa jadi rantai sertifikat belum lengkap (pakai fullchain.pem, bukan cert.pem).'); } }} catch { // Diabaikan: HTTPS belum tentu sudah aktif.}
// ------------------------------------------------------------------ ringkasanconst WARNA = { kritis: '\u001b[31m', tinggi: '\u001b[31m', sedang: '\u001b[33m', rendah: '\u001b[36m', info: '\u001b[2m' };const URUT = { kritis: 0, tinggi: 1, sedang: 2, rendah: 3, info: 4 };const R = '\u001b[0m';const w = (s, t) => (process.stdout.isTTY ? WARNA[s] + t + R : t);
console.log('');if (hasil.length === 0) { console.log('\u001b[32mSemua pemeriksaan lolos.\u001b[0m'); process.exit(0);}console.log(`HASIL: ${hasil.length} temuan`);for (const t of [...hasil].sort((a, b) => URUT[a.severity] - URUT[b.severity])) { console.log(`${w(t.severity, '[' + t.severity.toUpperCase() + ']')} ${t.judul}`); if (t.detail) for (const b of String(t.detail).split('\n')) console.log(` ${b}`);}process.exit(hasil.some((t) => t.severity === 'kritis') ? 1 : 0);