Lewati ke konten

Periksa GeoServer

Login, workspace, store, layer, akses publik, dan CORS.

Berkas: alat/hari-3/Praktik 7/tools/periksa-geoserver.mjs

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('');
// ---------------------------------------------------------------- 1
let 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}`);
}
// ---------------------------------------------------------------- 2
if (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}`);
}
// ---------------------------------------------------------------- 6
try {
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}`);
}
// ---------------------------------------------------------------- ringkasan
console.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);