mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
596 lines
17 KiB
JavaScript
596 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
'require poll';
|
|
'require rpc';
|
|
'require ui';
|
|
'require view';
|
|
|
|
var callStatus = rpc.declare({
|
|
object: 'safeshield',
|
|
method: 'status',
|
|
expect: { }
|
|
});
|
|
|
|
var callSetEnabled = rpc.declare({
|
|
object: 'safeshield',
|
|
method: 'set_enabled',
|
|
params: [ 'enabled' ],
|
|
expect: { }
|
|
});
|
|
|
|
var callRefresh = rpc.declare({
|
|
object: 'safeshield',
|
|
method: 'refresh',
|
|
expect: { }
|
|
});
|
|
|
|
const SUMMARY_MESSAGES = {
|
|
idle: _('SafeShield is idle.'),
|
|
ready: _('SafeShield is ready.'),
|
|
refreshing: _('SafeShield is refreshing the blocklist.'),
|
|
degraded: _('SafeShield is running with degraded protection.'),
|
|
error: _('SafeShield encountered an error.'),
|
|
paused: _('SafeShield is paused.'),
|
|
disabled: _('SafeShield is disabled.')
|
|
};
|
|
|
|
|
|
function apiError(response, fallback) {
|
|
if (response && response.error) {
|
|
if (response.error.message)
|
|
return response.error.message;
|
|
if (response.error.code)
|
|
return response.error.code;
|
|
}
|
|
|
|
return fallback || _('SafeShield request failed.');
|
|
}
|
|
|
|
function notify(level, message) {
|
|
ui.addNotification(null, E('p', {}, [ message ]), level || 'info');
|
|
}
|
|
|
|
function delay(ms) {
|
|
return new Promise(function(resolve) {
|
|
window.setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
function enabledStateConverged(status, target) {
|
|
var runtime = (status && status.runtime) || {};
|
|
|
|
if (target)
|
|
return status && status.enabled === true && status.active === true && runtime.refreshd_running === true;
|
|
|
|
return status && status.enabled === false && status.active === false && runtime.refreshd_running === false;
|
|
}
|
|
|
|
function waitForEnabledState(target, timeoutMs) {
|
|
var started = Date.now();
|
|
|
|
function check() {
|
|
return callStatus().then(function(status) {
|
|
if (enabledStateConverged(status, target) || Date.now() - started >= timeoutMs)
|
|
return status;
|
|
return delay(500).then(check);
|
|
});
|
|
}
|
|
|
|
return check();
|
|
}
|
|
|
|
function renderActions(status, root) {
|
|
var enabled = status && status.enabled === true;
|
|
var toggle = E('button', { 'class': 'btn cbi-button cbi-button-action' }, [ enabled ? _('Disable SafeShield') : _('Enable SafeShield') ]);
|
|
var refresh = E('button', { 'class': 'btn cbi-button cbi-button-positive' }, [ _('Refresh now') ]);
|
|
|
|
function replaceStatus(updated) {
|
|
root.replaceChild(renderStatus(updated, root), root.firstChild);
|
|
}
|
|
|
|
toggle.addEventListener('click', function(ev) {
|
|
var target = !enabled;
|
|
ev.preventDefault();
|
|
toggle.disabled = true;
|
|
|
|
callSetEnabled(target).then(function(response) {
|
|
if (!response || response.ok !== true || response.accepted !== true)
|
|
throw new Error(apiError(response, _('SafeShield rejected the lifecycle request.')));
|
|
|
|
notify('info', target ? _('Enable request accepted.') : _('Disable request accepted.'));
|
|
return waitForEnabledState(target, 15000);
|
|
}).then(function(updated) {
|
|
replaceStatus(updated);
|
|
if (enabledStateConverged(updated, target))
|
|
notify('info', target ? _('SafeShield is enabled and running.') : _('SafeShield is disabled and stopped.'));
|
|
else
|
|
notify('warning', _('SafeShield accepted the request but runtime convergence was not observed within 15 seconds. Status polling will continue.'));
|
|
}).catch(function(err) {
|
|
notify('danger', err.message || String(err));
|
|
}).finally(function() {
|
|
toggle.disabled = false;
|
|
});
|
|
});
|
|
|
|
refresh.disabled = !enabled;
|
|
refresh.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
refresh.disabled = true;
|
|
|
|
callRefresh().then(function(response) {
|
|
if (!response || response.ok !== true)
|
|
throw new Error(apiError(response, _('SafeShield refresh request failed.')));
|
|
|
|
if (response.accepted)
|
|
notify('info', _('SafeShield refresh started.'));
|
|
else if (response.reason === 'already_running')
|
|
notify('info', _('A SafeShield refresh is already running.'));
|
|
else
|
|
notify('warning', _('Refresh was not started: %s').format(response.reason || _('unknown reason')));
|
|
|
|
return callStatus();
|
|
}).then(replaceStatus).catch(function(err) {
|
|
notify('danger', err.message || String(err));
|
|
}).finally(function() {
|
|
refresh.disabled = !enabled;
|
|
});
|
|
});
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, [ _('Actions') ]),
|
|
E('div', { 'class': 'cbi-section-descr' }, [
|
|
_('Enable/disable and refresh actions use the public SafeShield ubus API. Enable/disable convergence is asynchronous and is verified through safeshield.status.')
|
|
]),
|
|
E('div', {}, [ toggle, ' ', refresh ])
|
|
]);
|
|
}
|
|
|
|
function asText(value, fallback) {
|
|
if (value === null || value === undefined || value === '')
|
|
return fallback || '-';
|
|
|
|
if (typeof value === 'boolean')
|
|
return value ? _('Yes') : _('No');
|
|
|
|
return String(value);
|
|
}
|
|
|
|
function formatBool(value) {
|
|
if (value === true || value === 1 || value === '1')
|
|
return _('Yes');
|
|
if (value === false || value === 0 || value === '0')
|
|
return _('No');
|
|
return '-';
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
if (value === null || value === undefined || value === '')
|
|
return '-';
|
|
return String(value);
|
|
}
|
|
|
|
function formatSizeKb(value) {
|
|
if (value === null || value === undefined || value === '')
|
|
return '-';
|
|
return String(value) + ' KB';
|
|
}
|
|
|
|
function formatHash(value) {
|
|
if (!value)
|
|
return '-';
|
|
|
|
var text = String(value);
|
|
return text.length > 24 ? text.substring(0, 16) + '…' + text.substring(text.length - 8) : text;
|
|
}
|
|
|
|
function formatTimestamp(value) {
|
|
var n, date;
|
|
|
|
if (!value)
|
|
return '-';
|
|
|
|
if (typeof value === 'number') {
|
|
n = value;
|
|
}
|
|
else if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
n = Number(value);
|
|
}
|
|
else {
|
|
date = new Date(value);
|
|
return isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
}
|
|
|
|
if (!n)
|
|
return '-';
|
|
|
|
/* SafeShield RPC returns Unix timestamps in seconds. JavaScript Date expects milliseconds. */
|
|
if (n < 100000000000)
|
|
n = n * 1000;
|
|
|
|
date = new Date(n);
|
|
return isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
}
|
|
|
|
function formatDuration(value) {
|
|
var n, days, hours, minutes, seconds, parts = [];
|
|
|
|
if (value === null || value === undefined || value === '')
|
|
return '-';
|
|
|
|
n = Number(value);
|
|
if (isNaN(n) || n < 0)
|
|
return String(value);
|
|
|
|
seconds = Math.floor(n);
|
|
days = Math.floor(seconds / 86400);
|
|
seconds = seconds % 86400;
|
|
hours = Math.floor(seconds / 3600);
|
|
seconds = seconds % 3600;
|
|
minutes = Math.floor(seconds / 60);
|
|
seconds = seconds % 60;
|
|
|
|
if (days)
|
|
parts.push(days + 'd');
|
|
if (hours)
|
|
parts.push(hours + 'h');
|
|
if (minutes)
|
|
parts.push(minutes + 'm');
|
|
if (seconds || !parts.length)
|
|
parts.push(seconds + 's');
|
|
|
|
return parts.join(' ');
|
|
}
|
|
|
|
function translateValue(value) {
|
|
switch (String(value || '').toLowerCase()) {
|
|
case 'idle': return _('Idle');
|
|
case 'scheduled_wait': return _('Waiting for scheduled refresh');
|
|
case 'boot_delay': return _('Waiting after boot');
|
|
case 'local_apply': return _('Applying local rules');
|
|
case 'local_merge': return _('Merging local rules');
|
|
case 'local_restart_dnsmasq': return _('Restarting DNS service');
|
|
case 'local_runtime_check': return _('Checking DNS service');
|
|
case 'local_blocklist_verify': return _('Verifying blocklist');
|
|
case 'ok': return _('OK');
|
|
case 'ready': return _('Ready');
|
|
case 'success': return _('Success');
|
|
case 'active': return _('Active');
|
|
case 'running': return _('Running');
|
|
case 'refreshing': return _('Refreshing');
|
|
case 'pending': return _('Pending');
|
|
case 'notice': return _('Notice');
|
|
case 'warning': return _('Warning');
|
|
case 'degraded': return _('Degraded');
|
|
case 'disabled': return _('Disabled');
|
|
case 'error': return _('Error');
|
|
case 'failed': return _('Failed');
|
|
case 'failure': return _('Failure');
|
|
case 'expired': return _('Expired');
|
|
case 'revoked': return _('Revoked');
|
|
case 'suspended': return _('Suspended');
|
|
case 'free': return _('Free');
|
|
case 'unlicensed': return _('Unlicensed');
|
|
case 'licensed': return _('Licensed');
|
|
case 'light': return _('Light');
|
|
case 'standard': return _('Standard');
|
|
case 'full': return _('Full');
|
|
case 'block': return _('Block');
|
|
case 'allow': return _('Allow');
|
|
default: return value;
|
|
}
|
|
}
|
|
|
|
function badgeClass(value) {
|
|
switch (String(value || '').toLowerCase()) {
|
|
case 'ok':
|
|
case 'ready':
|
|
case 'success':
|
|
case 'active':
|
|
return 'label success';
|
|
case 'running':
|
|
case 'refreshing':
|
|
case 'pending':
|
|
case 'notice':
|
|
case 'boot_delay':
|
|
case 'scheduled_wait':
|
|
case 'local_apply':
|
|
case 'local_merge':
|
|
case 'local_restart_dnsmasq':
|
|
case 'local_runtime_check':
|
|
case 'local_blocklist_verify':
|
|
return 'label notice';
|
|
case 'warning':
|
|
case 'warn':
|
|
case 'degraded':
|
|
case 'disabled':
|
|
return 'label warning';
|
|
case 'error':
|
|
case 'failed':
|
|
case 'failure':
|
|
case 'expired':
|
|
case 'revoked':
|
|
case 'suspended':
|
|
return 'label danger';
|
|
default:
|
|
return 'label';
|
|
}
|
|
}
|
|
|
|
function badge(value, fallback) {
|
|
var text = asText(value, fallback || '-');
|
|
return E('span', { 'class': badgeClass(text), 'style': 'padding: 0' }, [ translateValue(text) ]);
|
|
}
|
|
|
|
function row(label, value) {
|
|
var children;
|
|
|
|
if (Array.isArray(value))
|
|
children = value;
|
|
else if (value && typeof value === 'object')
|
|
children = [ value ];
|
|
else
|
|
children = [ asText(value) ];
|
|
|
|
return E('tr', {}, [
|
|
E('th', { 'style': 'width: 32%' }, [ label ]),
|
|
E('td', {}, children)
|
|
]);
|
|
}
|
|
|
|
function table(rows) {
|
|
return E('table', { 'class': 'table' }, rows);
|
|
}
|
|
|
|
function section(title, rows) {
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, [ title ]),
|
|
table(rows)
|
|
]);
|
|
}
|
|
|
|
function renderMessage(message) {
|
|
if (typeof message === 'string')
|
|
return message;
|
|
|
|
if (message && typeof message === 'object') {
|
|
if (message.message)
|
|
return message.message;
|
|
if (message.code)
|
|
return message.code;
|
|
return JSON.stringify(message);
|
|
}
|
|
|
|
return asText(message);
|
|
}
|
|
|
|
function renderMessages(title, items, className) {
|
|
if (!items || !items.length)
|
|
return null;
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, [ title ]),
|
|
E('ul', { 'class': className || '' }, items.map(function(item) {
|
|
return E('li', {}, [ renderMessage(item) ]);
|
|
}))
|
|
]);
|
|
}
|
|
|
|
function summaryMessage(summary) {
|
|
var code = String((summary && summary.code) || '').toLowerCase();
|
|
|
|
return SUMMARY_MESSAGES[code] || asText(summary && summary.message, '-');
|
|
}
|
|
|
|
function renderSummary(summary) {
|
|
if (!summary)
|
|
return '-';
|
|
|
|
if (typeof summary === 'string')
|
|
return translateValue(summary);
|
|
|
|
return [
|
|
E('span', { 'class': badgeClass(summary.severity || summary.label), 'style': 'padding: 0' }, [ translateValue(asText(summary.label, '-')) ]),
|
|
' ',
|
|
summaryMessage(summary)
|
|
];
|
|
}
|
|
|
|
function renderSources(sources) {
|
|
var items = (sources && sources.items) || [];
|
|
var body;
|
|
|
|
if (!items.length) {
|
|
body = [ E('tr', {}, [ E('td', { 'colspan': 8 }, [ _('No sources reported by SafeShield.') ]) ]) ];
|
|
}
|
|
else {
|
|
var cstyle = { 'style': 'text-align:center' };
|
|
body = items.map(function(item) {
|
|
return E('tr', {}, [
|
|
E('td', cstyle, [ asText(item.name || item.id) ]),
|
|
E('td', cstyle, [ translateValue(asText(item.action || sources.mode)) ]),
|
|
E('td', cstyle, [ formatBool(item.enabled) ]),
|
|
E('td', cstyle, [ badge(item.last_result) ]),
|
|
E('td', cstyle, [ formatNumber(item.line_count) ]),
|
|
E('td', cstyle, [ formatSizeKb(item.size_kb) ]),
|
|
E('td', cstyle, [ translateValue(asText(item.artifact_tier)) ]),
|
|
E('td', cstyle, [ asText(item.artifact_version) ])
|
|
]);
|
|
});
|
|
}
|
|
|
|
return E('div', { 'class': 'cbi-section' }, [
|
|
E('h3', {}, [ _('Sources') ]),
|
|
E('table', { 'class': 'table' }, [
|
|
E('tr', { 'class': 'tr table-titles' }, [
|
|
E('th', {}, [ _('Name') ]),
|
|
E('th', {}, [ _('Mode / action') ]),
|
|
E('th', {}, [ _('Enabled') ]),
|
|
E('th', {}, [ _('Last result') ]),
|
|
E('th', {}, [ _('Domains / lines') ]),
|
|
E('th', {}, [ _('Size') ]),
|
|
E('th', {}, [ _('Tier') ]),
|
|
E('th', {}, [ _('Version') ])
|
|
])
|
|
].concat(body))
|
|
]);
|
|
}
|
|
|
|
function renderHealth(checks) {
|
|
var names = [
|
|
[ 'api_resolve', _('Artifact metadata resolve') ],
|
|
[ 'artifact_download', _('Artifact download') ],
|
|
[ 'artifact_sha256', _('Artifact SHA-256') ],
|
|
[ 'dnsmasq_binary', _('dnsmasq binary') ],
|
|
[ 'dnsmasq_confdir', _('dnsmasq confdir') ],
|
|
[ 'dnsmasq_initial_restart', _('Initial dnsmasq restart') ],
|
|
[ 'dnsmasq_final_restart', _('Final dnsmasq restart') ],
|
|
[ 'dns_runtime', _('DNS runtime') ],
|
|
[ 'blocklist_verify', _('Blocklist verify') ],
|
|
[ 'min_valid_line_count', _('Minimum valid rule count') ],
|
|
[ 'max_file_size', _('Maximum file size') ]
|
|
];
|
|
|
|
return section(_('Health checks'), names.map(function(item) {
|
|
var value = checks ? checks[item[0]] : null;
|
|
return row(item[1], badge(value));
|
|
}));
|
|
}
|
|
|
|
function renderStatus(data, root) {
|
|
var status = data || {};
|
|
var runtime = status.runtime || {};
|
|
var license = status.license || {};
|
|
var device = status.device || {};
|
|
var deviceConfigured = device.configured || {};
|
|
var artifact = status.artifact || {};
|
|
var localOverrides = status.local_overrides || {};
|
|
var blocklist = status.blocklist || {};
|
|
var sources = status.sources || {};
|
|
var health = status.health || {};
|
|
var timestamps = status.timestamps || {};
|
|
var schema = status.schema || {};
|
|
|
|
var warnings = renderMessages(_('Warnings'), status.warnings, '');
|
|
var errors = renderMessages(_('Errors'), status.errors, '');
|
|
|
|
var nodes = [
|
|
E('h2', {}, [ _('SafeShield') ]),
|
|
renderActions(status, root),
|
|
|
|
section(_('Current status'), [
|
|
row(_('Status'), badge(status.status)),
|
|
row(_('Stage'), badge(status.stage)),
|
|
row(_('Summary'), renderSummary(status.summary)),
|
|
row(_('Version'), status.version),
|
|
row(_('Schema'), schema.name ? schema.name + ' v' + asText(schema.version) : '-'),
|
|
row(_('Enabled'), formatBool(status.enabled)),
|
|
row(_('Active'), formatBool(status.active)),
|
|
row(_('Health'), badge(health.overall)),
|
|
row(_('Generated at'), formatTimestamp(timestamps.generated_at)),
|
|
row(_('Last attempt'), formatTimestamp(timestamps.last_attempt)),
|
|
row(_('Last success'), formatTimestamp(timestamps.last_success)),
|
|
row(_('Last failure'), formatTimestamp(timestamps.last_failure)),
|
|
row(_('Last local apply'), formatTimestamp(timestamps.last_local_apply)),
|
|
row(_('Last local apply failure'), formatTimestamp(timestamps.last_local_apply_failure))
|
|
]),
|
|
|
|
section(_('Runtime'), [
|
|
row(_('Refresh daemon running'), formatBool(runtime.refreshd_running)),
|
|
row(_('dnsmasq running'), formatBool(runtime.dnsmasq_running)),
|
|
row(_('DNS runtime OK'), formatBool(runtime.dns_runtime_ok)),
|
|
row(_('Config loaded'), formatBool(runtime.config_loaded)),
|
|
row(_('Require WAN'), formatBool(runtime.require_wan)),
|
|
row(_('Refresh on boot'), formatBool(runtime.refresh_on_boot)),
|
|
row(_('Last result'), badge(runtime.last_result)),
|
|
row(_('Last error code'), runtime.last_error_code)
|
|
]),
|
|
|
|
|
|
section(_('License'), [
|
|
row(_('Configured'), formatBool(license.configured)),
|
|
row(_('Masked key'), license.key_masked),
|
|
row(_('Plan'), translateValue(asText(license.plan))),
|
|
row(_('Status'), badge(license.status))
|
|
]),
|
|
|
|
section(_('Device'), [
|
|
row(_('Physical fingerprint'), device.physical_fingerprint),
|
|
row(_('Runtime profile'), device.profile),
|
|
row(_('Configured physical fingerprint'), deviceConfigured.physical_fingerprint),
|
|
row(_('Vendor'), deviceConfigured.vendor),
|
|
row(_('Model'), deviceConfigured.model),
|
|
row(_('Architecture'), deviceConfigured.arch),
|
|
row(_('Memory'), deviceConfigured.memory_mb ? deviceConfigured.memory_mb + ' MB' : '-')
|
|
]),
|
|
|
|
section(_('Hub artifact'), [
|
|
row(_('Resolved'), formatBool(artifact.resolved)),
|
|
row(_('Tier'), translateValue(asText(artifact.tier))),
|
|
row(_('Version'), artifact.version),
|
|
row(_('SHA-256'), formatHash(artifact.sha256)),
|
|
row(_('Unique domains'), formatNumber(artifact.unique_domains)),
|
|
row(_('Rules'), formatNumber(artifact.rules)),
|
|
row(_('Download URL present'), formatBool(artifact.download_url_present))
|
|
]),
|
|
|
|
section(_('Local overrides'), [
|
|
row(_('Enabled'), formatBool(localOverrides.enabled)),
|
|
row(_('Allowlist path'), localOverrides.allowlist_path),
|
|
row(_('Blocklist path'), localOverrides.blocklist_path)
|
|
]),
|
|
|
|
section(_('Blocklist'), [
|
|
row(_('Installed'), formatBool(blocklist.installed)),
|
|
row(_('Path'), blocklist.path),
|
|
row(_('Valid rules'), formatNumber(blocklist.valid_line_count)),
|
|
row(_('File size'), formatSizeKb(blocklist.file_size_kb)),
|
|
row(_('Compressed'), formatBool(blocklist.compressed)),
|
|
row(_('Verification OK'), formatBool(blocklist.verification_ok)),
|
|
row(_('Test domain'), blocklist.test_domain),
|
|
row(_('Test domain success'), formatNumber(blocklist.test_domain_success_count) + ' / ' + formatNumber(blocklist.test_domain_sample_count)),
|
|
row(_('Previous backup available'), formatBool(blocklist.previous_backup_available))
|
|
]),
|
|
|
|
section(_('Schedule'), [
|
|
row(_('Refresh interval'), formatDuration(timestamps.refresh_interval_s)),
|
|
row(_('Next refresh'), formatTimestamp(timestamps.next_refresh_at)),
|
|
row(_('Next refresh in'), formatDuration(timestamps.next_refresh_in_s)),
|
|
row(_('Boot start delay'), formatDuration(timestamps.boot_start_delay_s))
|
|
]),
|
|
|
|
renderHealth(health.checks),
|
|
renderSources(sources)
|
|
];
|
|
|
|
if (warnings)
|
|
nodes.push(warnings);
|
|
if (errors)
|
|
nodes.push(errors);
|
|
|
|
return E('div', {}, nodes);
|
|
}
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return callStatus();
|
|
},
|
|
|
|
render: function(data) {
|
|
var root = E('div', {}, []);
|
|
|
|
root.appendChild(renderStatus(data, root));
|
|
|
|
poll.add(function() {
|
|
return callStatus().then(function(updated) {
|
|
root.replaceChild(renderStatus(updated, root), root.firstChild);
|
|
});
|
|
}, 5);
|
|
|
|
return root;
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|