195 lines
9.0 KiB
JavaScript
195 lines
9.0 KiB
JavaScript
import { h, PageHeader, Badge, StatusDot, Empty, Card, esc, enc, att_esc, $val, apiFetch, toast, dismissToast, openModal, closeModal, formModal, definePage, hComp, connect, reactive, requestUpdate, Link, createRouter, ToastContainer, render, parseZones } from '/static/hoover/index.js';
|
|
|
|
function addFwdModal(zones, state) {
|
|
openModal((inner, idx) => {
|
|
formModal(inner, 'Add Port Forward',
|
|
[
|
|
{ label: 'Zone', id: 'fwd-zone', tag: 'select', options: zones.map(z => [z, z === zones[0]]) },
|
|
{ label: 'Port', id: 'fwd-port', type: 'number' },
|
|
{ label: 'Protocol', id: 'fwd-proto', placeholder: 'tcp or udp' },
|
|
{ label: 'To Address', id: 'fwd-toaddr', placeholder: '192.168.1.100' },
|
|
{ label: 'To Port (optional)', id: 'fwd-toport', type: 'number' },
|
|
],
|
|
[
|
|
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) },
|
|
{
|
|
label: 'Add', cls: 'btn-primary', action: 's', handler: async () => {
|
|
const body = {
|
|
zone: $val('fwd-zone'),
|
|
port: parseInt($val('fwd-port')),
|
|
proto: ($val('fwd-proto') || 'tcp').trim(),
|
|
toaddr: ($val('fwd-toaddr') || '').trim() || undefined,
|
|
toport: $val('fwd-toport') ? parseInt($val('fwd-toport')) : undefined,
|
|
};
|
|
if (!body.zone || !body.port || !body.proto) {
|
|
toast('Zone, port, and proto are required', 'error');
|
|
return;
|
|
}
|
|
const r = await apiFetch('/api/firewall/forward-port', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (r.ok) {
|
|
toast('Forward rule added', 'success');
|
|
closeModal(idx);
|
|
await load(state);
|
|
} else {
|
|
toast(r.error || 'Failed', 'error');
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
async function load(state, abortController, entry) {
|
|
if (Object.keys(state.config || {}).length) state.refreshing = true;
|
|
else state.loading = true;
|
|
try {
|
|
const myId = entry ? entry.requestId : 0;
|
|
const sig = abortController?.signal;
|
|
const r = await apiFetch('/api/firewall/config', { signal: sig });
|
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
|
if (r.ok) state.config = r.data || {};
|
|
const zr = await apiFetch('/api/firewall/zones', { signal: sig });
|
|
if (abortController?.signal.aborted || (entry && entry.requestId !== myId)) return;
|
|
if (zr.ok) state.activeZones = Object.keys(zr.data?.active || {});
|
|
} catch (e) {
|
|
if (abortController?.signal.aborted) return;
|
|
state.error = String(e);
|
|
}
|
|
state.loading = false;
|
|
state.refreshing = false;
|
|
}
|
|
|
|
export default definePage({
|
|
init() {
|
|
return { config: {}, activeZones: [], loading: true, refreshing: false, error: null };
|
|
},
|
|
subscribe: ['firewall'],
|
|
load,
|
|
render(state) {
|
|
if (state.loading && !state.refreshing) {
|
|
return [
|
|
PageHeader({ title: 'NAT', subtitle: 'Masquerade & port forwarding' }),
|
|
h('div', { class: 'card', key: 'loading' },
|
|
h('div', { class: 'card-body loading' }, state.refreshing ? 'Refreshing...' : 'Loading...'),
|
|
),
|
|
];
|
|
}
|
|
|
|
if (state.error) {
|
|
return [
|
|
PageHeader({ title: 'NAT' }),
|
|
h('div', { class: 'card', key: 'error' },
|
|
h('div', { class: 'card-body error-msg' }, state.error),
|
|
),
|
|
];
|
|
}
|
|
|
|
const cfg = state.config || {};
|
|
const zoneData = cfg.zones || {};
|
|
|
|
const masqRows = Object.entries(zoneData).map(([zone, zcfg]) => {
|
|
const masq = !!zcfg.masquerade;
|
|
return h('tr', { key: 'm-' + zone },
|
|
h('td', null, h('strong', null, zone)),
|
|
h('td', null, Badge({ text: masq ? 'Enabled' : 'Disabled', variant: masq ? 'success' : 'info' })),
|
|
h('td', null,
|
|
h('button', { class: 'btn btn-sm btn-outline',
|
|
'on:click': async () => {
|
|
const r = await apiFetch('/api/firewall/masquerade', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ zone, enable: !masq }),
|
|
});
|
|
if (r.ok) {
|
|
toast('Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone, 'success');
|
|
await load(state);
|
|
} else {
|
|
toast(r.error || 'Failed', 'error');
|
|
}
|
|
}}, masq ? 'Disable' : 'Enable'),
|
|
),
|
|
);
|
|
});
|
|
|
|
const fwRows = [];
|
|
Object.entries(zoneData).forEach(([zone, zcfg]) => {
|
|
const forwards = zcfg.forward_ports || [];
|
|
forwards.forEach((fwd, i) => {
|
|
fwRows.push(h('tr', { key: 'f-' + zone + '-' + i },
|
|
h('td', null, h('strong', null, zone)),
|
|
h('td', null, Badge({ text: fwd['proxy-protocol'] || fwd.proto || 'tcp', variant: 'info' })),
|
|
h('td', null, fwd.port),
|
|
h('td', null, fwd['to-addr'] || fwd.toaddr || '-'),
|
|
h('td', null, fwd['to-port'] || fwd.toport || '-'),
|
|
h('td', null,
|
|
h('button', { class: 'btn btn-sm btn-danger',
|
|
'on:click': async () => {
|
|
const port = fwd.port, proto = fwd['proxy-protocol'] || fwd.proto;
|
|
if (!confirm('Remove forward ' + zone + ':' + port + '/' + proto + '?')) return;
|
|
const r = await apiFetch('/api/firewall/forward-port/' + enc(zone) + '/' + port + '/' + enc(proto), { method: 'DELETE' });
|
|
if (r.ok) {
|
|
toast('Rule removed', 'success');
|
|
await load(state);
|
|
} else {
|
|
toast(r.error || 'Failed', 'error');
|
|
}
|
|
}}, 'Remove'),
|
|
),
|
|
));
|
|
});
|
|
});
|
|
|
|
return [
|
|
PageHeader({ title: 'NAT', subtitle: 'Masquerade & port forwarding' }),
|
|
h('h3', { class: 'section-title' }, 'Masquerade'),
|
|
h('div', { class: 'card' },
|
|
h('table', { class: 'table' },
|
|
h('thead', null,
|
|
h('tr', null,
|
|
h('th', null, 'Zone'),
|
|
h('th', null, 'Status'),
|
|
h('th', { style: 'width:100px;' }, 'Action'),
|
|
),
|
|
),
|
|
h('tbody', null,
|
|
...(masqRows.length ? masqRows : [
|
|
h('tr', null, h('td', { colspan: 3, class: 'text-muted' }, 'No zones')),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
h('h3', { class: 'section-title' }, 'Port Forwarding'),
|
|
h('div', { class: 'card' },
|
|
h('div', { style: 'padding:0.75rem;', class: 'flex' },
|
|
h('button', { class: 'btn btn-sm btn-primary',
|
|
'on:click': () => addFwdModal(state.activeZones, state) }, 'Add Forward'),
|
|
),
|
|
h('table', { class: 'table' },
|
|
h('thead', null,
|
|
h('tr', null,
|
|
h('th', null, 'Zone'),
|
|
h('th', null, 'Proto'),
|
|
h('th', null, 'Port'),
|
|
h('th', null, 'To Addr'),
|
|
h('th', null, 'To Port'),
|
|
h('th', { style: 'width:80px;' }, 'Action'),
|
|
),
|
|
),
|
|
h('tbody', null,
|
|
...(fwRows.length ? fwRows : [
|
|
h('tr', null,
|
|
h('td', { colspan: 6, class: 'text-muted text-sm' }, 'No port forwarding rules'),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
];
|
|
},
|
|
});
|