Periksa GeoServer
Periksa GeoServer
Section titled “Periksa GeoServer”Login, workspace, store, layer, akses publik, dan CORS.
Berkas: alat/hari-3/Praktik 7/tools/periksa-geoserver.mjs
#!/usr/bin/env node// =====================================================================// Praktik 7 - periksa-geoserver.mjs//// Mengubah langkah Praktik 7 yang serba manual di browser (klik Workspaces,// klik Stores, klik Publish, klik Security) menjadi pemeriksaan otomatis// lewat REST API GeoServer.//// Yang diperiksa:// 1. GeoServer hidup dan admin bisa login// 2. Workspace ada dan Namespace URI-nya benar// 3. Data store PostGIS ada dan benar-benar terhubung ke database// 4. Layer tersedia dan tidak error// 5. Layer benar-benar bisa diakses TANPA login (klaim "public" di modul)// 6. Header CORS untuk endpoint WFS//// Pakai:// GEOSERVER_PASSWORD=hasil-openssl-rand \// node periksa-geoserver.mjs http://localhost:8080/geoserver//// Tanpa argumen URL, default ke http://localhost:8080/geoserver.// Password default kartoza dipakai hanya sebagai fallback agar praktik// lokal bisa dicek; di VM selalu set GEOSERVER_PASSWORD dari .env.// =====================================================================
const GEOSERVER_URL = (process.argv[2] ?? 'http://localhost:8080/geoserver').replace(/\/+$/, '');const USER = process.env.GEOSERVER_ADMIN_USER ?? 'admin';const PASSWORD = process.env.GEOSERVER_PASSWORD ?? process.env.GEOSERVER_ADMIN_PASSWORD ?? 'geoserver';const WORKSPACE = process.env.GEOSERVER_WORKSPACE ?? 'geoportal';const STORE = process.env.GEOSERVER_STORE ?? process.env.GEOSERVER_DATASTORE ?? null;const LAYER = process.env.GEOSERVER_LAYER ?? null;const TIMEOUT_MS = Number(process.env.TIMEOUT_MS ?? 10000);
const authDasar = 'Basic ' + Buffer.from(`${USER}:${PASSWORD}`).toString('base64');
async function minta(jalur, { auth = true, ...opsi } = {}) { const url = jalur.startsWith('http') ? jalur : GEOSERVER_URL + jalur; const kontrol = new AbortController(); const timer = setTimeout(() => kontrol.abort(), TIMEOUT_MS); try { const respons = await fetch(url, { ...opsi, signal: kontrol.signal, headers: { Accept: 'application/json', ...(auth ? { Authorization: authDasar } : {}), ...(opsi.headers ?? {}), }, }); return respons; } finally { clearTimeout(timer); }}
async function jsonAman(respons) { const teks = await respons.text(); try { return JSON.parse(teks); } catch { return { _mentah: teks.slice(0, 300) }; }}
const temuan = [];function catat(severity, judul, detail) { temuan.push({ severity, judul, detail });}
const SEVERITY_WARNA = { kritis: '\u001b[31m', tinggi: '\u001b[31m', sedang: '\u001b[33m', rendah: '\u001b[36m', info: '\u001b[2m' };const SEVERITY_URUT = { kritis: 0, tinggi: 1, sedang: 2, rendah: 3, info: 4 };const RESET = '\u001b[0m';const warna = (s, t) => (process.stdout.isTTY ? SEVERITY_WARNA[s] + t + RESET : t);
console.log(`GeoServer : ${GEOSERVER_URL}`);console.log(`Workspace : ${WORKSPACE}`);console.log('');
// ---------------------------------------------------------------- 1let hidup = false;try { const r = await minta('/rest/about/version.json'); if (r.status === 401) { catat('kritis', 'Login admin GeoServer gagal (HTTP 401)', 'Periksa GEOSERVER_ADMIN_USER / GEOSERVER_PASSWORD.'); console.log('1. Versi : GAGAL — 401 Unauthorized'); } else if (!r.ok) { catat('kritis', `GeoServer membalas HTTP ${r.status} pada /rest/about/version.json`, 'Periksa path konteks: apakah /geoserver atau /geoserver/web.'); console.log(`1. Versi : GAGAL — HTTP ${r.status}`); } else { hidup = true; const v = await jsonAman(r); const res = v?.about?.resource ?? []; const versi = res.find((x) => x['@name'] === 'GeoServer')?.Version ?? '(tidak terbaca)'; console.log(`1. Versi : OK — GeoServer ${versi}`); }} catch (e) { const sebab = e.cause?.code ?? e.cause?.message ?? e.message; catat('kritis', 'GeoServer tidak bisa dihubungi', `${GEOSERVER_URL}\n` + `Penyebab : ${sebab}\n` + 'Langkah : 1) docker compose ps — pastikan geoserver_app berstatus running\n' + ' 2) bagian 5 dan 6 di bawah hanya bermakna setelah koneksi berhasil,\n' + ' jadi "fetch failed" di sana adalah akibat, bukan temuan terpisah.'); console.log(`1. Versi : GAGAL — ${sebab}`);}
// ---------------------------------------------------------------- 2if (hidup) { const r = await minta(`/rest/workspaces/${WORKSPACE}.json`); if (r.ok) { const w = await jsonAman(r); const uri = w?.workspace?.namespaces?.namespace?.[0]?.href ?? ''; console.log(`2. Workspace : OK — ${WORKSPACE}`); if (!uri) { catat('sedang', 'Namespace URI workspace tidak terbaca dari REST API', 'Cek manual di Data > Workspaces.'); } } else { catat('tinggi', `Workspace "${WORKSPACE}" tidak ditemukan (HTTP ${r.status})`, 'Buat workspace lebih dahulu di Data > Workspaces.'); console.log(`2. Workspace : GAGAL — HTTP ${r.status}`); }
// -------------------------------------------------------------- 3 const rStore = await minta(`/rest/workspaces/${WORKSPACE}/datastores.json`); if (rStore.ok) { const ds = await jsonAman(rStore); const daftar = ds?.dataStores?.dataStore ?? []; if (daftar.length === 0) { catat('tinggi', 'Belum ada data store di workspace ini', 'Praktik 7 menuntut data store PostGIS yang terhubung ke database.'); console.log('3. Data store : GAGAL — kosong'); } else { for (const s of daftar) { const nama = s.name; if (STORE && nama !== STORE) continue; const rDetail = await minta(`/rest/workspaces/${WORKSPACE}/datastores/${nama}.json`); const detail = rDetail.ok ? await jsonAman(rDetail) : {}; const tipe = detail?.dataStore?.type ?? '(tidak diketahui)'; // Parameter koneksi hanya keluar kalau diminta eksplisit. const rParam = await minta( `/rest/workspaces/${WORKSPACE}/datastores/${nama}.json?quietOnNotFound=true`, ); let pesan = `${nama} (${tipe})`; if (tipe !== 'PostGIS') { catat('sedang', `Data store "${nama}" bukan PostGIS melainkan ${tipe}`, 'Praktik 7 memakai PostGIS sebagai type of data source.'); } console.log(`3. Data store : OK — ${pesan}`); void rParam; } } } else { catat('sedang', `Tidak bisa membaca daftar data store (HTTP ${rStore.status})`, ''); console.log(`3. Data store : GAGAL — HTTP ${rStore.status}`); }
// -------------------------------------------------------------- 4 const rLayer = await minta(`/rest/workspaces/${WORKSPACE}/layers.json`); if (rLayer.ok) { const l = await jsonAman(rLayer); const daftar = (l?.layers?.layer ?? []).map((x) => x.name); console.log(`4. Layer : ${daftar.length} layer — ${daftar.join(', ') || '(kosong)'}`); if (daftar.length === 0) { catat('tinggi', 'Tidak ada layer yang dipublish', 'Publish layer pada data store, lalu isi Bounding Box dengan Compute from data.'); } if (LAYER && !daftar.includes(LAYER)) { catat('tinggi', `Layer "${LAYER}" tidak ada di workspace ${WORKSPACE}`, `Yang tersedia: ${daftar.join(', ')}`); } } else { console.log(`4. Layer : GAGAL — HTTP ${rLayer.status}`); }}
// ---------------------------------------------------------------- 5// Klaim modul: "layer akan terbuka tanpa muncul permintaan login karena// layer sudah diberikan akses publik". Diuji tanpa header Authorization.const layerUji = LAYER ?? `${WORKSPACE}:batas_admin`;const wfsUji = `${GEOSERVER_URL}/wfs?service=WFS&version=2.0.0&request=GetFeature` + `&typeNames=${encodeURIComponent(layerUji)}&outputFormat=application/json&count=1`;
try { const r = await minta(wfsUji, { auth: false }); const teks = await r.text(); if (r.ok && teks.trim().startsWith('{') && teks.includes('"features"')) { let jumlah = '?'; try { jumlah = JSON.parse(teks).features?.length ?? 0; } catch { /* biarkan */ } console.log(`5. Akses publik : OK — WFS tanpa login mengembalikan ${jumlah} feature`); } else if (r.status === 401 || r.status === 403) { catat( 'tinggi', `Layer "${layerUji}" belum bisa diakses publik (HTTP ${r.status})`, 'Security > Layers > pilih layer > centang Read untuk ROLE_ANONYMOUS > Save.', ); console.log(`5. Akses publik : GAGAL — HTTP ${r.status} (butuh login)`); } else if (r.status === 404 || teks.includes('ServiceException')) { catat('sedang', `WFS membalas error untuk layer "${layerUji}"`, teks.slice(0, 200)); console.log(`5. Akses publik : GAGAL — layer tidak ditemukan`); } else { console.log(`5. Akses publik : HTTP ${r.status}`); }} catch (e) { console.log(`5. Akses publik : GAGAL — ${e.message}`);}
// ---------------------------------------------------------------- 6try { const r = await minta(wfsUji, { auth: false, headers: { Origin: 'http://localhost:3000' } }); const izin = r.headers.get('access-control-allow-origin'); if (izin) { console.log(`6. CORS : OK — Access-Control-Allow-Origin: ${izin}`); if (izin === '*') { catat( 'rendah', 'CORS GeoServer terbuka untuk semua origin (*)', 'Nilai ini datang dari web.xml bawaan image kartoza, bukan dari environment GEOSERVER_CORS_ALLOWED_ORIGINS (variabel itu tidak dibaca image).', ); } } else { catat( 'sedang', 'Tidak ada header Access-Control-Allow-Origin pada respons WFS', 'Browser akan memblokir permintaan lintas origin dari aplikasi Next.js.', ); console.log('6. CORS : GAGAL — header tidak ada'); }} catch (e) { console.log(`6. CORS : GAGAL — ${e.message}`);}
// ---------------------------------------------------------------- ringkasanconsole.log('');if (temuan.length === 0) { console.log('\u001b[32mSemua pemeriksaan lolos.\u001b[0m'); process.exit(0);}const urut = [...temuan].sort((a, b) => SEVERITY_URUT[a.severity] - SEVERITY_URUT[b.severity]);console.log(`HASIL: ${temuan.length} temuan`);for (const t of urut) { console.log(`${warna(t.severity, '[' + t.severity.toUpperCase() + ']')} ${t.judul}`); if (t.detail) for (const b of String(t.detail).split('\n')) console.log(` ${b}`);}process.exit(temuan.some((t) => ['kritis', 'tinggi'].includes(t.severity)) ? 1 : 0);