This commit is contained in:
+130
-4
@@ -17,7 +17,12 @@ async function api(path, body) {
|
||||
/* ── composables ───────────────────────────────────────────── */
|
||||
|
||||
function useRules() {
|
||||
const rules = ref({ mode: 'blacklist', blocked: [], allowed: [], enforce: true });
|
||||
// форма объекта должна совпадать с серверной с самого начала: `save` шлёт
|
||||
// правила целиком, и отсутствующее здесь поле сервер прочитал бы как пустое
|
||||
const rules = ref({
|
||||
mode: 'blacklist', blocked: [], allowed: [], enforce: true,
|
||||
blocked_sites: [], sites_enforce: false, dns_lockdown: false,
|
||||
});
|
||||
const load = async () => { rules.value = await api('/rules'); };
|
||||
const block = (name) => api('/rules/block', { name }).then(load);
|
||||
const allow = (name) => api('/rules/allow', { name }).then(load);
|
||||
@@ -54,6 +59,19 @@ function useLog() {
|
||||
return { lines, load };
|
||||
}
|
||||
|
||||
function useSites() {
|
||||
const dns = ref({
|
||||
listening: false, upstream: '', queries: 0, blocked: 0,
|
||||
cache_hits: 0, timeouts: 0, error: null, attempts: [],
|
||||
});
|
||||
const load = async () => { dns.value = await api('/dns'); };
|
||||
// сам список закрытых сайтов живёт в правилах, здесь — только состояние
|
||||
// резолвера и попытки; поэтому после правки перечитываем и то, и другое
|
||||
const block = (name) => api('/sites/block', { name });
|
||||
const remove = (name) => api('/sites/remove', { name });
|
||||
return { dns, load, block, remove };
|
||||
}
|
||||
|
||||
function useUpdate() {
|
||||
const info = ref({ current: '', latest: null, available: false, phase: 'idle', checked_at: null, error: null });
|
||||
const load = async () => { info.value = await api('/update'); };
|
||||
@@ -152,6 +170,95 @@ const RulesPanel = {
|
||||
</section>`,
|
||||
};
|
||||
|
||||
const SitesPanel = {
|
||||
components: { NameChips },
|
||||
props: {
|
||||
sites: { type: Array, default: () => [] },
|
||||
dns: { type: Object, required: true },
|
||||
// enabled — сама галочка, active — работает ли блокировка на деле:
|
||||
// общий выключатель «применять правила» гасит и её
|
||||
enabled: { type: Boolean, default: false },
|
||||
active: { type: Boolean, default: false },
|
||||
lockdown: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['block', 'remove', 'enable', 'lockdown'],
|
||||
setup(props, { emit }) {
|
||||
const input = ref('');
|
||||
const submit = () => {
|
||||
const value = input.value.trim();
|
||||
if (!value) return;
|
||||
emit('block', value);
|
||||
input.value = '';
|
||||
};
|
||||
|
||||
const status = computed(() => {
|
||||
const d = props.dns;
|
||||
if (props.enabled && !props.active) return 'выключено общей галочкой «применять правила»';
|
||||
if (!props.active) return 'выключено — служба имена не разрешает';
|
||||
if (d.error) return d.error;
|
||||
if (!d.listening) return 'резолвер запускается…';
|
||||
return `спрашиваю ${d.upstream} · запросов ${d.queries}, ` +
|
||||
`отклонено ${d.blocked}, из кэша ${d.cache_hits}` +
|
||||
(d.timeouts ? `, без ответа ${d.timeouts}` : '');
|
||||
});
|
||||
|
||||
const ago = (s) => (s < 60 ? `${s} с назад` : `${Math.floor(s / 60)} мин назад`);
|
||||
|
||||
return {
|
||||
input, submit, status, ago,
|
||||
// тревожно только то, что должно работать, но не работает
|
||||
bad: computed(() => props.active && (props.dns.error || !props.dns.listening)),
|
||||
idle: computed(() => !props.active && props.sites.length > 0),
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between">
|
||||
<h2 style="margin:0">Сайты</h2>
|
||||
<label class="row">
|
||||
<input type="checkbox" :checked="enabled"
|
||||
@change="$emit('enable', $event.target.checked)">
|
||||
блокировать сайты
|
||||
</label>
|
||||
</div>
|
||||
<div :class="bad ? 'warn' : 'muted'" style="margin:8px 0 10px">{{ status }}</div>
|
||||
<p v-if="idle" class="warn" style="margin:0 0 10px">
|
||||
Список составлен, но блокировка выключена — сайты открываются.
|
||||
</p>
|
||||
<div class="row">
|
||||
<input v-model="input" placeholder="discord.com" style="flex:1;min-width:140px"
|
||||
@keyup.enter="submit">
|
||||
<button @click="submit">Закрыть</button>
|
||||
</div>
|
||||
<p class="muted" style="margin:8px 0 0">
|
||||
Запись закрывает и поддомены: <code>discord.com</code> закроет и
|
||||
<code>gateway.discord.com</code>.
|
||||
</p>
|
||||
<name-chips title="Закрытые сайты" :names="sites" @remove="$emit('remove', $event)" />
|
||||
<label class="row" style="margin-top:12px">
|
||||
<input type="checkbox" :checked="lockdown" :disabled="!active"
|
||||
@change="$emit('lockdown', $event.target.checked)">
|
||||
<span :class="{ muted: !active }">запретить обход через чужие серверы имён</span>
|
||||
</label>
|
||||
<p v-if="lockdown && active" class="warn" style="margin:6px 0 0">
|
||||
Правило брандмауэра закрывает 53-й порт всем, кроме службы. Если интернет
|
||||
пропадёт — снимите галочку.
|
||||
</p>
|
||||
<div v-if="dns.attempts.length" class="scroll" style="margin-top:12px;max-height:180px">
|
||||
<table>
|
||||
<thead><tr><th>Куда ломились</th><th>Раз</th><th>Когда</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="a in dns.attempts" :key="a.name">
|
||||
<td>{{ a.name }}</td>
|
||||
<td>{{ a.count }}</td>
|
||||
<td class="muted">{{ ago(a.ago_secs) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>`,
|
||||
};
|
||||
|
||||
const RandomKill = {
|
||||
emits: ['schedule'],
|
||||
setup(_, { emit }) {
|
||||
@@ -247,15 +354,16 @@ const EventLog = {
|
||||
/* ── корневой компонент ────────────────────────────────────── */
|
||||
|
||||
const App = {
|
||||
components: { UpdateBar, RulesPanel, RandomKill, ProcessTable, EventLog },
|
||||
components: { UpdateBar, RulesPanel, SitesPanel, RandomKill, ProcessTable, EventLog },
|
||||
setup() {
|
||||
const { rules, load: loadRules, block, allow, remove, save } = useRules();
|
||||
const { groups, load: loadProcesses, kill } = useProcesses();
|
||||
const { lines, load: loadLog } = useLog();
|
||||
const { info: update, load: loadUpdate, check: checkUpdate } = useUpdate();
|
||||
const { dns, load: loadDns, block: blockSite, remove: removeSite } = useSites();
|
||||
|
||||
onMounted(loadRules);
|
||||
usePolling(() => { loadProcesses(); loadLog(); }, 3000);
|
||||
usePolling(() => { loadProcesses(); loadLog(); loadDns(); }, 3000);
|
||||
// версия меняется раз в сутки в лучшем случае — частить незачем
|
||||
usePolling(loadUpdate, 30000);
|
||||
|
||||
@@ -276,7 +384,18 @@ const App = {
|
||||
|
||||
const whitelist = computed(() => rules.value.mode === 'whitelist');
|
||||
|
||||
return { rules, groups, lines, update, whitelist, save, remove, toggle, kill, scheduleRandom, checkUpdate };
|
||||
// правка списка сайтов меняет и правила, и показания резолвера
|
||||
const onSite = async (action, name) => {
|
||||
await action(name);
|
||||
await Promise.all([loadRules(), loadDns()]);
|
||||
};
|
||||
|
||||
return {
|
||||
rules, groups, lines, update, dns, whitelist,
|
||||
save, remove, toggle, kill, scheduleRandom, checkUpdate,
|
||||
blockSite: (name) => onSite(blockSite, name),
|
||||
removeSite: (name) => onSite(removeSite, name),
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<div class="row" style="justify-content:space-between">
|
||||
@@ -285,6 +404,13 @@ const App = {
|
||||
</div>
|
||||
<update-bar :info="update" @check="checkUpdate" />
|
||||
<rules-panel :rules="rules" @update="save" @remove="remove" />
|
||||
<sites-panel :sites="rules.blocked_sites" :dns="dns"
|
||||
:enabled="rules.sites_enforce"
|
||||
:active="rules.enforce && rules.sites_enforce"
|
||||
:lockdown="rules.dns_lockdown"
|
||||
@block="blockSite" @remove="removeSite"
|
||||
@enable="save({ sites_enforce: $event })"
|
||||
@lockdown="save({ dns_lockdown: $event })" />
|
||||
<random-kill @schedule="scheduleRandom" />
|
||||
<process-table :groups="groups" :whitelist="whitelist" @toggle="toggle" @kill="kill" />
|
||||
<event-log :lines="lines" />`,
|
||||
|
||||
+5
-1
@@ -39,11 +39,15 @@
|
||||
.warn { color:#e0b872; }
|
||||
pre { margin:0; font:12px/1.6 ui-monospace, Consolas, monospace; color:#9aa4b2;
|
||||
white-space:pre-wrap; max-height:200px; overflow:auto; }
|
||||
code { font:12px/1.5 ui-monospace, Consolas, monospace; color:#c8d0da;
|
||||
background:#232833; border-radius:4px; padding:1px 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" v-cloak></div>
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<!-- Vue отдаёт сама служба: панель не должна зависеть от сети, тем более от
|
||||
разрешения имён, которым служба и управляет. -->
|
||||
<script src="/vue.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user