mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:24:34 +08:00
🎉 Sync 2026-09-27 21:03:26
This commit is contained in:
1 parent
f4559bbed1
commit
065763f7fd
673 files changed
+261285
-34160
No files matched your search
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
// Keep private SafeShield RPC modules next to this rpcd entrypoint.
|
||||
// Prepend the private module directory to the ucode require() search path.
|
||||
unshift(REQUIRE_SEARCH_PATH, '/usr/share/rpcd/ucode/safeshield/*.uc');
|
||||
|
||||
let status = require('status');
|
||||
let config = require('config');
|
||||
let refresh = require('refresh');
|
||||
let rules = require('rules');
|
||||
let license = require('license');
|
||||
let statistics = require('statistics');
|
||||
|
||||
return {
|
||||
safeshield: {
|
||||
status: {
|
||||
call: status.build
|
||||
},
|
||||
|
||||
config: {
|
||||
call: config.build
|
||||
},
|
||||
|
||||
statistics: {
|
||||
call: statistics.build
|
||||
},
|
||||
|
||||
config_update: {
|
||||
args: {
|
||||
values: {}
|
||||
},
|
||||
call: config.update
|
||||
},
|
||||
|
||||
set_enabled: {
|
||||
args: {
|
||||
enabled: true
|
||||
},
|
||||
call: config.set_enabled
|
||||
},
|
||||
|
||||
refresh: {
|
||||
call: refresh.call
|
||||
},
|
||||
|
||||
rules_list: {
|
||||
args: {
|
||||
action: ''
|
||||
},
|
||||
call: rules.list
|
||||
},
|
||||
|
||||
rule_add: {
|
||||
args: {
|
||||
action: '',
|
||||
domain: '',
|
||||
refresh: true
|
||||
},
|
||||
call: rules.add
|
||||
},
|
||||
|
||||
rule_delete: {
|
||||
args: {
|
||||
action: '',
|
||||
domain: '',
|
||||
refresh: true
|
||||
},
|
||||
call: rules.delete
|
||||
},
|
||||
|
||||
license_get: {
|
||||
call: license.get
|
||||
},
|
||||
|
||||
license_update: {
|
||||
args: {
|
||||
license_key: ''
|
||||
},
|
||||
call: license.update
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,276 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
|
||||
let uci = core.uci;
|
||||
let PKG_NAME = core.PKG_NAME;
|
||||
let CONFIG_SCHEMA_NAME = core.CONFIG_SCHEMA_NAME;
|
||||
let CONFIG_SCHEMA_VERSION = core.CONFIG_SCHEMA_VERSION;
|
||||
let CONFIG_SPEC = core.CONFIG_SPEC;
|
||||
let reload_uci = core.reload_uci;
|
||||
let cfg = core.cfg;
|
||||
let to_bool = core.to_bool;
|
||||
let to_int = core.to_int;
|
||||
let mask_secret = core.mask_secret;
|
||||
let api_error = core.api_error;
|
||||
let uci_commit_option = core.uci_commit_option;
|
||||
let bool_uci = core.bool_uci;
|
||||
let run_service_action = runtime.run_service_action;
|
||||
let start_refresh_async = runtime.start_refresh_async;
|
||||
|
||||
function config_public_value(name, spec) {
|
||||
let raw = cfg(name, null);
|
||||
|
||||
if (spec.kind == 'bool') {
|
||||
return to_bool(raw, spec.def);
|
||||
}
|
||||
|
||||
return to_int(raw, spec.def);
|
||||
}
|
||||
|
||||
function build_config() {
|
||||
reload_uci();
|
||||
|
||||
let values = {};
|
||||
|
||||
for (let name, spec in CONFIG_SPEC) {
|
||||
values[name] = config_public_value(name, spec);
|
||||
}
|
||||
|
||||
values.enabled = to_bool(cfg('enabled', '0'), false);
|
||||
|
||||
return {
|
||||
schema: {
|
||||
name: CONFIG_SCHEMA_NAME,
|
||||
version: CONFIG_SCHEMA_VERSION
|
||||
},
|
||||
values: values,
|
||||
license: {
|
||||
configured: !!cfg('license_key', ''),
|
||||
key_masked: mask_secret(cfg('license_key', ''))
|
||||
},
|
||||
device: {
|
||||
vendor: cfg('device_vendor', ''),
|
||||
model: cfg('device_model', ''),
|
||||
arch: cfg('device_arch', ''),
|
||||
memory_mb: to_int(cfg('device_memory_mb', ''), 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validate_config_value(name, value) {
|
||||
let spec = CONFIG_SPEC[name];
|
||||
|
||||
if (!spec) {
|
||||
if (name == 'enabled') {
|
||||
return api_error('dedicated_method_required', 'Use set_enabled to change enabled state', name);
|
||||
}
|
||||
|
||||
if (name == 'license_key') {
|
||||
return api_error('dedicated_method_required', 'Use license_update to change the license key', name);
|
||||
}
|
||||
|
||||
return api_error('unknown_option', 'Unsupported SafeShield configuration option', name);
|
||||
}
|
||||
|
||||
if (spec.kind == 'bool') {
|
||||
if (type(value) != 'bool') {
|
||||
return api_error('invalid_type', 'Expected a boolean value', name);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
uci: bool_uci(value),
|
||||
value: value
|
||||
};
|
||||
}
|
||||
|
||||
if (spec.kind == 'int') {
|
||||
if (type(value) != 'int') {
|
||||
return api_error('invalid_type', 'Expected an integer value', name);
|
||||
}
|
||||
|
||||
if (value < spec.min || value > spec.max) {
|
||||
return api_error(
|
||||
'out_of_range',
|
||||
sprintf('Value must be between %d and %d', spec.min, spec.max),
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
uci: sprintf('%d', value),
|
||||
value: value
|
||||
};
|
||||
}
|
||||
|
||||
return api_error('invalid_spec', 'Invalid internal configuration specification', name);
|
||||
}
|
||||
|
||||
function config_update_call(request) {
|
||||
reload_uci();
|
||||
|
||||
let values = request.args.values || {};
|
||||
let changes = {};
|
||||
let changed_names = [];
|
||||
|
||||
if (type(values) != 'object') {
|
||||
return api_error('invalid_type', 'values must be an object', 'values');
|
||||
}
|
||||
|
||||
for (let name, value in values) {
|
||||
let checked = validate_config_value(name, value);
|
||||
|
||||
if (!checked.ok) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
let current = cfg(name, null);
|
||||
if (sprintf('%s', current) != checked.uci) {
|
||||
changes[name] = checked.uci;
|
||||
push(changed_names, name);
|
||||
}
|
||||
}
|
||||
|
||||
if (length(changed_names) == 0) {
|
||||
return {
|
||||
ok: true,
|
||||
changed: [],
|
||||
restarted: false,
|
||||
config: build_config()
|
||||
};
|
||||
}
|
||||
|
||||
for (let name, value in changes) {
|
||||
if (!uci.set(PKG_NAME, 'config', name, value)) {
|
||||
uci.revert(PKG_NAME);
|
||||
return api_error('uci_set_failed', 'Failed to update SafeShield configuration', name);
|
||||
}
|
||||
}
|
||||
|
||||
if (!uci.commit(PKG_NAME)) {
|
||||
uci.revert(PKG_NAME);
|
||||
return api_error('uci_commit_failed', 'Failed to commit SafeShield configuration');
|
||||
}
|
||||
|
||||
reload_uci();
|
||||
|
||||
let statistics_only =
|
||||
length(changed_names) == 1 &&
|
||||
changed_names[0] == 'statistics_enabled';
|
||||
|
||||
if (statistics_only) {
|
||||
let reconciled = run_service_action('reconcile_statistics', 60000);
|
||||
if (!reconciled.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
committed: true,
|
||||
changed: changed_names,
|
||||
restarted: false,
|
||||
reconciled: false,
|
||||
service_rc: reconciled.rc,
|
||||
error: {
|
||||
code: 'statistics_reconcile_failed',
|
||||
message: 'Configuration was committed but SafeShield statistics runtime reconciliation failed'
|
||||
},
|
||||
config: build_config()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
changed: changed_names,
|
||||
restarted: false,
|
||||
reconciled: true,
|
||||
refresh: {
|
||||
requested: false,
|
||||
accepted: false,
|
||||
reason: 'not_required'
|
||||
},
|
||||
config: build_config()
|
||||
};
|
||||
}
|
||||
|
||||
let restarted = run_service_action('restart', 60000);
|
||||
if (!restarted.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
committed: true,
|
||||
changed: changed_names,
|
||||
restarted: false,
|
||||
service_rc: restarted.rc,
|
||||
error: {
|
||||
code: 'service_restart_failed',
|
||||
message: 'Configuration was committed but SafeShield failed to restart'
|
||||
},
|
||||
config: build_config()
|
||||
};
|
||||
}
|
||||
|
||||
let refresh = start_refresh_async();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
changed: changed_names,
|
||||
restarted: true,
|
||||
refresh: {
|
||||
requested: true,
|
||||
accepted: refresh.accepted,
|
||||
reason: refresh.reason || ''
|
||||
},
|
||||
config: build_config()
|
||||
};
|
||||
}
|
||||
|
||||
function set_enabled_call(request) {
|
||||
reload_uci();
|
||||
|
||||
if (request.args.enabled == null) {
|
||||
return api_error('missing_argument', 'enabled is required', 'enabled');
|
||||
}
|
||||
|
||||
if (type(request.args.enabled) != 'bool') {
|
||||
return api_error('invalid_type', 'enabled must be a boolean', 'enabled');
|
||||
}
|
||||
|
||||
let enabled = request.args.enabled;
|
||||
let current = to_bool(cfg('enabled', '0'), false);
|
||||
let changed = enabled != current;
|
||||
|
||||
if (changed && !uci_commit_option('enabled', bool_uci(enabled))) {
|
||||
return api_error('uci_commit_failed', 'Failed to update enabled state');
|
||||
}
|
||||
|
||||
let restarted = run_service_action('restart', 60000);
|
||||
if (!restarted.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
committed: changed,
|
||||
changed: changed,
|
||||
accepted: false,
|
||||
target_enabled: enabled,
|
||||
reconciled: false,
|
||||
service_rc: restarted.rc,
|
||||
error: {
|
||||
code: 'service_restart_failed',
|
||||
message: 'Enabled state is configured but SafeShield failed to request runtime lifecycle reconciliation'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
changed: changed,
|
||||
accepted: true,
|
||||
target_enabled: enabled,
|
||||
reconciled: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
build: build_config,
|
||||
update: config_update_call,
|
||||
set_enabled: set_enabled_call
|
||||
};
|
||||
@@ -0,0 +1,250 @@
|
||||
'use strict';
|
||||
|
||||
let fs = require('fs');
|
||||
let uci = require('uci').cursor();
|
||||
|
||||
const PKG_NAME = 'safeshield';
|
||||
const STATUS_FILE = `/dev/shm/${PKG_NAME}.status.json`;
|
||||
const BLOCKLIST_FILE = '/tmp/dnsmasq.d/safeshield.blocklist';
|
||||
const LOCAL_ALLOWLIST_FILE = '/etc/safeshield/allowlist';
|
||||
const LOCAL_BLOCKLIST_FILE = '/etc/safeshield/blocklist';
|
||||
const SCHEMA_NAME = 'safeshield.status';
|
||||
const SCHEMA_VERSION = 1;
|
||||
const CONFIG_SCHEMA_NAME = 'safeshield.config';
|
||||
const CONFIG_SCHEMA_VERSION = 1;
|
||||
const RULES_SCHEMA_NAME = 'safeshield.rules';
|
||||
const RULES_SCHEMA_VERSION = 1;
|
||||
const STATISTICS_SCHEMA_NAME = 'safeshield.statistics';
|
||||
const STATISTICS_SCHEMA_VERSION = 3;
|
||||
const STATISTICS_FILE = '/tmp/safeshield/statistics/statistics.json';
|
||||
const SERVICE_INIT = '/etc/init.d/safeshield';
|
||||
const RULES_DIR = '/etc/safeshield';
|
||||
|
||||
const CONFIG_SPEC = {
|
||||
verbosity: { kind: 'int', def: 2, min: 0, max: 7 },
|
||||
apply_local_overrides: { kind: 'bool', def: true },
|
||||
max_blocklist_file_size_kb: { kind: 'int', def: 30000, min: 1, max: 2147483647 },
|
||||
min_valid_line_count: { kind: 'int', def: 3000, min: 0, max: 2147483647 },
|
||||
compress_blocklist: { kind: 'bool', def: false },
|
||||
initial_dnsmasq_restart: { kind: 'bool', def: false },
|
||||
dnsmasq_sanity_check: { kind: 'bool', def: true },
|
||||
statistics_enabled: { kind: 'bool', def: true },
|
||||
statistics_snapshot_interval_s: { kind: 'int', def: 60, min: 10, max: 3600 },
|
||||
statistics_retention_hours: { kind: 'int', def: 168, min: 1, max: 168 },
|
||||
download_timeout: { kind: 'int', def: 10, min: 1, max: 86400 },
|
||||
download_retry: { kind: 'int', def: 3, min: 1, max: 100 },
|
||||
pause_timeout: { kind: 'int', def: 20, min: 1, max: 86400 },
|
||||
boot_start_delay_s: { kind: 'int', def: 30, min: 0, max: 86400 },
|
||||
refresh_on_boot: { kind: 'bool', def: true },
|
||||
refresh_interval_s: { kind: 'int', def: 28800, min: 1, max: 2147483647 },
|
||||
require_wan: { kind: 'bool', def: true },
|
||||
debug: { kind: 'bool', def: false }
|
||||
};
|
||||
|
||||
function trim(s) {
|
||||
return replace(s, /^[\r\n\t ]+|[\r\n\t ]+$/g, '');
|
||||
}
|
||||
|
||||
function pkg_version() {
|
||||
let s = fs.readfile(`/usr/lib/${PKG_NAME}/version`);
|
||||
if (!s) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return trim(s);
|
||||
}
|
||||
|
||||
const PKG_VERSION = pkg_version();
|
||||
|
||||
function read_json_file(path, def) {
|
||||
let s = fs.readfile(path);
|
||||
if (!s) {
|
||||
return def;
|
||||
}
|
||||
|
||||
try {
|
||||
return json(s);
|
||||
}
|
||||
catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
function file_exists(path) {
|
||||
let st = fs.stat(path);
|
||||
return !!st;
|
||||
}
|
||||
|
||||
function file_size_kb(path) {
|
||||
let st = fs.stat(path);
|
||||
if (!st || type(st.size) != 'int') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return int((st.size + 1023) / 1024);
|
||||
}
|
||||
|
||||
function to_bool(v, def) {
|
||||
if (v == null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
switch (sprintf('%s', v)) {
|
||||
case '1':
|
||||
case 'true':
|
||||
case 'yes':
|
||||
case 'on':
|
||||
return true;
|
||||
case '0':
|
||||
case 'false':
|
||||
case 'no':
|
||||
case 'off':
|
||||
return false;
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
function to_optional_bool(v) {
|
||||
if (v == null || v == '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (sprintf('%s', v)) {
|
||||
case '1':
|
||||
case 'true':
|
||||
case 'yes':
|
||||
case 'on':
|
||||
return true;
|
||||
case '0':
|
||||
case 'false':
|
||||
case 'no':
|
||||
case 'off':
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function to_int(v, def) {
|
||||
if (v == null || v == '') {
|
||||
return def;
|
||||
}
|
||||
|
||||
let n = int(v);
|
||||
return (type(n) == 'int') ? n : def;
|
||||
}
|
||||
|
||||
function reload_uci() {
|
||||
return !!uci.load(PKG_NAME);
|
||||
}
|
||||
|
||||
function cfg(name, def) {
|
||||
let v = uci.get(PKG_NAME, 'config', name);
|
||||
return (v == null) ? def : v;
|
||||
}
|
||||
|
||||
function identity_cfg(name, def) {
|
||||
let v = uci.get(PKG_NAME, 'identity', name);
|
||||
return (v == null) ? def : v;
|
||||
}
|
||||
|
||||
function mask_secret(v) {
|
||||
let s = sprintf('%s', v || '');
|
||||
let n = length(s);
|
||||
|
||||
if (n == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (n <= 8) {
|
||||
return '********';
|
||||
}
|
||||
|
||||
return sprintf('%s...%s', substr(s, 0, 4), substr(s, n - 4));
|
||||
}
|
||||
|
||||
function api_error(code, message, field) {
|
||||
let error = {
|
||||
code: code,
|
||||
message: message
|
||||
};
|
||||
|
||||
if (field) {
|
||||
error.field = field;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
|
||||
function uci_commit_option(name, value) {
|
||||
if (!uci.set(PKG_NAME, 'config', name, sprintf('%s', value))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uci.commit(PKG_NAME)) {
|
||||
uci.revert(PKG_NAME);
|
||||
return false;
|
||||
}
|
||||
|
||||
reload_uci();
|
||||
return true;
|
||||
}
|
||||
|
||||
function uci_delete_option(name) {
|
||||
if (!uci.delete(PKG_NAME, 'config', name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uci.commit(PKG_NAME)) {
|
||||
uci.revert(PKG_NAME);
|
||||
return false;
|
||||
}
|
||||
|
||||
reload_uci();
|
||||
return true;
|
||||
}
|
||||
|
||||
function bool_uci(v) {
|
||||
return v ? '1' : '0';
|
||||
}
|
||||
|
||||
return {
|
||||
uci: uci,
|
||||
PKG_NAME: PKG_NAME,
|
||||
PKG_VERSION: PKG_VERSION,
|
||||
STATUS_FILE: STATUS_FILE,
|
||||
BLOCKLIST_FILE: BLOCKLIST_FILE,
|
||||
LOCAL_ALLOWLIST_FILE: LOCAL_ALLOWLIST_FILE,
|
||||
LOCAL_BLOCKLIST_FILE: LOCAL_BLOCKLIST_FILE,
|
||||
SCHEMA_NAME: SCHEMA_NAME,
|
||||
SCHEMA_VERSION: SCHEMA_VERSION,
|
||||
CONFIG_SCHEMA_NAME: CONFIG_SCHEMA_NAME,
|
||||
CONFIG_SCHEMA_VERSION: CONFIG_SCHEMA_VERSION,
|
||||
RULES_SCHEMA_NAME: RULES_SCHEMA_NAME,
|
||||
RULES_SCHEMA_VERSION: RULES_SCHEMA_VERSION,
|
||||
STATISTICS_SCHEMA_NAME: STATISTICS_SCHEMA_NAME,
|
||||
STATISTICS_SCHEMA_VERSION: STATISTICS_SCHEMA_VERSION,
|
||||
STATISTICS_FILE: STATISTICS_FILE,
|
||||
SERVICE_INIT: SERVICE_INIT,
|
||||
RULES_DIR: RULES_DIR,
|
||||
CONFIG_SPEC: CONFIG_SPEC,
|
||||
trim: trim,
|
||||
read_json_file: read_json_file,
|
||||
file_exists: file_exists,
|
||||
file_size_kb: file_size_kb,
|
||||
to_bool: to_bool,
|
||||
to_optional_bool: to_optional_bool,
|
||||
to_int: to_int,
|
||||
reload_uci: reload_uci,
|
||||
cfg: cfg,
|
||||
identity_cfg: identity_cfg,
|
||||
mask_secret: mask_secret,
|
||||
api_error: api_error,
|
||||
uci_commit_option: uci_commit_option,
|
||||
uci_delete_option: uci_delete_option,
|
||||
bool_uci: bool_uci
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
|
||||
let reload_uci = core.reload_uci;
|
||||
let cfg = core.cfg;
|
||||
let trim = core.trim;
|
||||
let mask_secret = core.mask_secret;
|
||||
let api_error = core.api_error;
|
||||
let uci_commit_option = core.uci_commit_option;
|
||||
let uci_delete_option = core.uci_delete_option;
|
||||
let start_refresh_async = runtime.start_refresh_async;
|
||||
let reset_statistics_upload_state = runtime.reset_statistics_upload_state;
|
||||
|
||||
function license_get_call() {
|
||||
reload_uci();
|
||||
|
||||
let key = cfg('license_key', '');
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
license: {
|
||||
configured: !!key,
|
||||
key: key
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function license_update_call(request) {
|
||||
reload_uci();
|
||||
|
||||
if (request.args.license_key == null) {
|
||||
return api_error('missing_argument', 'license_key is required; use an empty string to clear it', 'license_key');
|
||||
}
|
||||
|
||||
let key = trim(request.args.license_key);
|
||||
|
||||
if (length(key) > 512 || match(key, /[\r\n]/)) {
|
||||
return api_error('invalid_license_key', 'license_key must be at most 512 characters and contain no line breaks', 'license_key');
|
||||
}
|
||||
|
||||
let current = cfg('license_key', '');
|
||||
|
||||
if (current == key) {
|
||||
return {
|
||||
ok: true,
|
||||
changed: false,
|
||||
license: {
|
||||
configured: !!key,
|
||||
key_masked: mask_secret(key)
|
||||
},
|
||||
refresh: {
|
||||
requested: false,
|
||||
accepted: false,
|
||||
reason: 'unchanged'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let saved = key == ''
|
||||
? uci_delete_option('license_key')
|
||||
: uci_commit_option('license_key', key);
|
||||
|
||||
if (!saved) {
|
||||
return api_error('uci_commit_failed', 'Failed to update the SafeShield license key');
|
||||
}
|
||||
|
||||
reset_statistics_upload_state();
|
||||
let refresh = start_refresh_async();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
changed: true,
|
||||
license: {
|
||||
configured: !!key,
|
||||
key_masked: mask_secret(key)
|
||||
},
|
||||
refresh: {
|
||||
requested: true,
|
||||
accepted: refresh.accepted,
|
||||
reason: refresh.reason || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
get: license_get_call,
|
||||
update: license_update_call
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
let runtime = require('runtime');
|
||||
let status = require('status');
|
||||
|
||||
let start_refresh_async = runtime.start_refresh_async;
|
||||
let build_status = status.build;
|
||||
|
||||
function refresh_call() {
|
||||
let r = start_refresh_async();
|
||||
|
||||
if (!r.accepted) {
|
||||
return {
|
||||
ok: r.reason == 'already_running',
|
||||
accepted: false,
|
||||
reason: r.reason,
|
||||
status: build_status()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
accepted: true,
|
||||
status: build_status()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
call: refresh_call
|
||||
};
|
||||
@@ -0,0 +1,316 @@
|
||||
'use strict';
|
||||
|
||||
let fs = require('fs');
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
|
||||
let LOCAL_ALLOWLIST_FILE = core.LOCAL_ALLOWLIST_FILE;
|
||||
let LOCAL_BLOCKLIST_FILE = core.LOCAL_BLOCKLIST_FILE;
|
||||
let RULES_SCHEMA_NAME = core.RULES_SCHEMA_NAME;
|
||||
let RULES_SCHEMA_VERSION = core.RULES_SCHEMA_VERSION;
|
||||
let RULES_DIR = core.RULES_DIR;
|
||||
let trim = core.trim;
|
||||
let reload_uci = core.reload_uci;
|
||||
let cfg = core.cfg;
|
||||
let to_bool = core.to_bool;
|
||||
let api_error = core.api_error;
|
||||
let start_local_apply_async = runtime.start_local_apply_async;
|
||||
|
||||
function normalize_rule_domain(value) {
|
||||
if (type(value) != 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let domain = lc(trim(value));
|
||||
|
||||
if (!domain || substr(domain, 0, 1) == '#' || substr(domain, 0, 1) == '!') {
|
||||
return '';
|
||||
}
|
||||
|
||||
domain = replace(domain, /^0\.0\.0\.0[ \t]+/, '');
|
||||
domain = replace(domain, /^127\.0\.0\.1[ \t]+/, '');
|
||||
domain = replace(domain, /^local=\//, '');
|
||||
domain = replace(domain, /^address=\//, '');
|
||||
domain = replace(domain, /\/0\.0\.0\.0$/, '');
|
||||
domain = replace(domain, /\/::$/, '');
|
||||
domain = replace(domain, /\/$/, '');
|
||||
domain = replace(domain, /^\//, '');
|
||||
|
||||
if (!match(domain, /^[a-z0-9._-]+$/) || length(domain) > 253 || index(domain, '..') >= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (let label in split(domain, '.')) {
|
||||
if (!label || length(label) > 63 || substr(label, 0, 1) == '-' || substr(label, -1) == '-') {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
function rule_path(action) {
|
||||
if (action == 'allow') {
|
||||
return LOCAL_ALLOWLIST_FILE;
|
||||
}
|
||||
|
||||
if (action == 'block') {
|
||||
return LOCAL_BLOCKLIST_FILE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function read_rule_entries(path) {
|
||||
let content = fs.readfile(path) || '';
|
||||
let entries = [];
|
||||
let seen = {};
|
||||
|
||||
for (let line in split(content, /\n/)) {
|
||||
let domain = normalize_rule_domain(line);
|
||||
|
||||
if (!domain || seen[domain]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen[domain] = true;
|
||||
push(entries, domain);
|
||||
}
|
||||
|
||||
sort(entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
function write_rule_file_atomic(path, content) {
|
||||
if (!fs.stat(RULES_DIR) && !fs.mkdir(RULES_DIR)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tmp = sprintf('%s.tmp.%d', path, time());
|
||||
|
||||
if (fs.writefile(tmp, content) == null) {
|
||||
fs.unlink(tmp);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fs.chmod(tmp, 0o644)) {
|
||||
fs.unlink(tmp);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fs.rename(tmp, path)) {
|
||||
fs.unlink(tmp);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function rule_file_contains(path, domain) {
|
||||
for (let existing in read_rule_entries(path)) {
|
||||
if (existing == domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function add_rule_to_file(path, domain) {
|
||||
let content = fs.readfile(path) || '';
|
||||
|
||||
if (rule_file_contains(path, domain)) {
|
||||
return {
|
||||
ok: true,
|
||||
changed: false
|
||||
};
|
||||
}
|
||||
|
||||
if (content && substr(content, -1) != '\n') {
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
content += domain + '\n';
|
||||
|
||||
return {
|
||||
ok: write_rule_file_atomic(path, content),
|
||||
changed: true
|
||||
};
|
||||
}
|
||||
|
||||
function delete_rule_from_file(path, domain) {
|
||||
let content = fs.readfile(path) || '';
|
||||
let lines = split(content, /\n/);
|
||||
let kept = [];
|
||||
let changed = false;
|
||||
|
||||
for (let line in lines) {
|
||||
if (normalize_rule_domain(line) == domain) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
push(kept, line);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return {
|
||||
ok: true,
|
||||
changed: false
|
||||
};
|
||||
}
|
||||
|
||||
let next = join('\n', kept);
|
||||
|
||||
return {
|
||||
ok: write_rule_file_atomic(path, next),
|
||||
changed: true
|
||||
};
|
||||
}
|
||||
|
||||
function build_rules(action) {
|
||||
let allow = read_rule_entries(LOCAL_ALLOWLIST_FILE);
|
||||
let block = read_rule_entries(LOCAL_BLOCKLIST_FILE);
|
||||
|
||||
if (action == 'allow') {
|
||||
block = [];
|
||||
}
|
||||
else if (action == 'block') {
|
||||
allow = [];
|
||||
}
|
||||
|
||||
return {
|
||||
schema: {
|
||||
name: RULES_SCHEMA_NAME,
|
||||
version: RULES_SCHEMA_VERSION
|
||||
},
|
||||
allow: allow,
|
||||
block: block,
|
||||
counts: {
|
||||
allow: length(allow),
|
||||
block: length(block)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validate_rule_request(args) {
|
||||
let action = args.action || '';
|
||||
let domain = normalize_rule_domain(args.domain);
|
||||
|
||||
if (!rule_path(action)) {
|
||||
return api_error('invalid_action', 'action must be either allow or block', 'action');
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return api_error('invalid_domain', 'domain must be a plain valid domain name', 'domain');
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
action: action,
|
||||
domain: domain,
|
||||
path: rule_path(action)
|
||||
};
|
||||
}
|
||||
|
||||
function apply_rule_refresh(args) {
|
||||
reload_uci();
|
||||
|
||||
let should_refresh = (args.refresh == null) ? true : args.refresh;
|
||||
|
||||
if (!should_refresh) {
|
||||
return {
|
||||
requested: false,
|
||||
accepted: false,
|
||||
reason: 'not_requested'
|
||||
};
|
||||
}
|
||||
|
||||
if (!to_bool(cfg('apply_local_overrides', '1'), true)) {
|
||||
return {
|
||||
requested: false,
|
||||
accepted: false,
|
||||
reason: 'local_overrides_disabled'
|
||||
};
|
||||
}
|
||||
|
||||
let r = start_local_apply_async();
|
||||
|
||||
return {
|
||||
requested: true,
|
||||
accepted: r.accepted,
|
||||
reason: r.reason || ''
|
||||
};
|
||||
}
|
||||
|
||||
function rules_list_call(request) {
|
||||
let action = request.args.action || '';
|
||||
|
||||
if (action && !rule_path(action)) {
|
||||
return api_error('invalid_action', 'action must be allow, block or omitted', 'action');
|
||||
}
|
||||
|
||||
let result = build_rules(action);
|
||||
result.ok = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
function rule_add_call(request) {
|
||||
let checked = validate_rule_request(request.args);
|
||||
|
||||
if (!checked.ok) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
let result = add_rule_to_file(checked.path, checked.domain);
|
||||
|
||||
if (!result.ok) {
|
||||
return api_error('rule_write_failed', 'Failed to update the local rule file');
|
||||
}
|
||||
|
||||
let refresh = result.changed
|
||||
? apply_rule_refresh(request.args)
|
||||
: { requested: false, accepted: false, reason: 'unchanged' };
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
action: checked.action,
|
||||
domain: checked.domain,
|
||||
added: result.changed,
|
||||
refresh: refresh,
|
||||
rules: build_rules(checked.action)
|
||||
};
|
||||
}
|
||||
|
||||
function rule_delete_call(request) {
|
||||
let checked = validate_rule_request(request.args);
|
||||
|
||||
if (!checked.ok) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
let result = delete_rule_from_file(checked.path, checked.domain);
|
||||
|
||||
if (!result.ok) {
|
||||
return api_error('rule_write_failed', 'Failed to update the local rule file');
|
||||
}
|
||||
|
||||
let refresh = result.changed
|
||||
? apply_rule_refresh(request.args)
|
||||
: { requested: false, accepted: false, reason: 'unchanged' };
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
action: checked.action,
|
||||
domain: checked.domain,
|
||||
deleted: result.changed,
|
||||
refresh: refresh,
|
||||
rules: build_rules(checked.action)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
list: rules_list_call,
|
||||
add: rule_add_call,
|
||||
delete: rule_delete_call
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let fs = require('fs');
|
||||
let ubus = require('ubus').connect();
|
||||
|
||||
let PKG_NAME = core.PKG_NAME;
|
||||
let STATUS_FILE = core.STATUS_FILE;
|
||||
let SERVICE_INIT = core.SERVICE_INIT;
|
||||
let read_json_file = core.read_json_file;
|
||||
let to_bool = core.to_bool;
|
||||
let reload_uci = core.reload_uci;
|
||||
let cfg = core.cfg;
|
||||
|
||||
function service_running(name) {
|
||||
let r = ubus.call('service', 'list', { name: name });
|
||||
if (!r || !r[name] || !r[name].instances) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let inst_name, inst in r[name].instances) {
|
||||
if (inst.running) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function service_instance_running(name, instance_name) {
|
||||
let r = ubus.call('service', 'list', { name: name });
|
||||
if (!r || !r[name] || !r[name].instances || !r[name].instances[instance_name]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!r[name].instances[instance_name].running;
|
||||
}
|
||||
|
||||
function dnsmasq_running() {
|
||||
let r = ubus.call('service', 'list', { name: 'dnsmasq' });
|
||||
if (!r || !r.dnsmasq || !r.dnsmasq.instances) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let inst_name, inst in r.dnsmasq.instances) {
|
||||
if (inst.running) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function run_service_action(action, timeout_ms) {
|
||||
let rc = system([ SERVICE_INIT, action ], timeout_ms || 60000);
|
||||
|
||||
return {
|
||||
ok: rc == 0,
|
||||
rc: rc
|
||||
};
|
||||
}
|
||||
|
||||
function refresh_running() {
|
||||
let state = read_json_file(STATUS_FILE, {});
|
||||
let data = state.data || {};
|
||||
|
||||
return data.status == 'running';
|
||||
}
|
||||
|
||||
function reset_statistics_upload_state() {
|
||||
fs.unlink('/tmp/safeshield/statistics/upload.credentials');
|
||||
fs.unlink('/tmp/safeshield/statistics/upload.entitlement');
|
||||
fs.unlink('/tmp/safeshield/statistics/upload.pending.json');
|
||||
fs.unlink('/tmp/safeshield/statistics/upload.pending.meta');
|
||||
fs.unlink('/tmp/safeshield/statistics/upload.state');
|
||||
}
|
||||
|
||||
function start_refresh_async() {
|
||||
reload_uci();
|
||||
|
||||
if (!to_bool(cfg('enabled', '0'), false)) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'disabled'
|
||||
};
|
||||
}
|
||||
|
||||
if (!service_running(PKG_NAME)) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'service_stopped'
|
||||
};
|
||||
}
|
||||
|
||||
if (refresh_running()) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'already_running'
|
||||
};
|
||||
}
|
||||
|
||||
let rc = system([
|
||||
'/bin/sh',
|
||||
'-c',
|
||||
'/etc/init.d/safeshield refresh_once </dev/null >/dev/null 2>&1 &'
|
||||
], 5000);
|
||||
|
||||
return {
|
||||
accepted: rc == 0,
|
||||
reason: (rc == 0) ? '' : 'spawn_failed',
|
||||
rc: rc
|
||||
};
|
||||
}
|
||||
|
||||
function start_local_apply_async() {
|
||||
reload_uci();
|
||||
|
||||
if (!to_bool(cfg('enabled', '0'), false)) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'disabled'
|
||||
};
|
||||
}
|
||||
|
||||
if (!to_bool(cfg('apply_local_overrides', '1'), true)) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'local_overrides_disabled'
|
||||
};
|
||||
}
|
||||
|
||||
if (!service_running(PKG_NAME)) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'service_stopped'
|
||||
};
|
||||
}
|
||||
|
||||
// The shell worker shares the refresh lock with full artifact refreshes.
|
||||
// It waits for an in-flight refresh and then merges the newest local rule
|
||||
// files against the retained api.block.txt cache. Duplicate workers are
|
||||
// harmless because the engine fingerprints the normalized local state.
|
||||
let rc = system([
|
||||
'/bin/sh',
|
||||
'-c',
|
||||
'/etc/init.d/safeshield apply_local_rules </dev/null >/dev/null 2>&1 &'
|
||||
], 5000);
|
||||
|
||||
return {
|
||||
accepted: rc == 0,
|
||||
reason: (rc == 0) ? '' : 'spawn_failed',
|
||||
rc: rc
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
service_running: service_running,
|
||||
service_instance_running: service_instance_running,
|
||||
dnsmasq_running: dnsmasq_running,
|
||||
run_service_action: run_service_action,
|
||||
refresh_running: refresh_running,
|
||||
reset_statistics_upload_state: reset_statistics_upload_state,
|
||||
start_refresh_async: start_refresh_async,
|
||||
start_local_apply_async: start_local_apply_async
|
||||
};
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
|
||||
let PKG_NAME = core.PKG_NAME;
|
||||
let STATISTICS_SCHEMA_NAME = core.STATISTICS_SCHEMA_NAME;
|
||||
let STATISTICS_SCHEMA_VERSION = core.STATISTICS_SCHEMA_VERSION;
|
||||
let STATISTICS_FILE = core.STATISTICS_FILE;
|
||||
let reload_uci = core.reload_uci;
|
||||
let cfg = core.cfg;
|
||||
let read_json_file = core.read_json_file;
|
||||
let to_bool = core.to_bool;
|
||||
let to_int = core.to_int;
|
||||
let service_instance_running = runtime.service_instance_running;
|
||||
function sanitize_hourly(items) {
|
||||
let result = [];
|
||||
|
||||
if (type(items) != 'array') {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (let item in items) {
|
||||
if (type(item) != 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
push(result, {
|
||||
bucket_start: to_int(item.bucket_start, 0),
|
||||
queries: to_int(item.queries, 0),
|
||||
blocked: to_int(item.blocked, 0)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitize_devices(items) {
|
||||
let result = [];
|
||||
if (type(items) != 'array') {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (let item in items) {
|
||||
if (type(item) != 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let id = sprintf('%s', item.id || '');
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
push(result, {
|
||||
id: id,
|
||||
mac: sprintf('%s', item.mac || ''),
|
||||
ip: sprintf('%s', item.ip || ''),
|
||||
hostname: sprintf('%s', item.hostname || ''),
|
||||
identified: to_bool(item.identified, false),
|
||||
queries: to_int(item.queries, 0),
|
||||
blocked: to_int(item.blocked, 0),
|
||||
hourly: sanitize_hourly(item.hourly)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitize_source(item) {
|
||||
if (type(item) != 'object') {
|
||||
item = {};
|
||||
}
|
||||
|
||||
return {
|
||||
backend: sprintf('%s', item.backend || 'dnsmasq_ubus'),
|
||||
available: to_bool(item.available, false),
|
||||
instance_id: sprintf('%s', item.instance_id || ''),
|
||||
transport_scope: sprintf('%s', item.transport_scope || ''),
|
||||
client_capacity: to_int(item.client_capacity, 0),
|
||||
tracked_clients: to_int(item.tracked_clients, 0),
|
||||
untracked_queries: to_int(item.untracked_queries, 0),
|
||||
untracked_blocked: to_int(item.untracked_blocked, 0),
|
||||
poll_error_count: to_int(item.poll_error_count, 0),
|
||||
last_error_at: to_int(item.last_error_at, 0)
|
||||
};
|
||||
}
|
||||
|
||||
function build_statistics() {
|
||||
reload_uci();
|
||||
let enabled = to_bool(cfg('statistics_enabled', '1'), true);
|
||||
let retention_hours = to_int(cfg('statistics_retention_hours', '168'), 168);
|
||||
let snapshot_interval_s = to_int(cfg('statistics_snapshot_interval_s', '60'), 60);
|
||||
let data = read_json_file(STATISTICS_FILE, {});
|
||||
let totals = (type(data.totals) == 'object') ? data.totals : {};
|
||||
let collector_running = service_instance_running(PKG_NAME, 'statistics');
|
||||
let effective_enabled = enabled && collector_running;
|
||||
let source = sanitize_source(data.source);
|
||||
if (!effective_enabled) {
|
||||
source.available = false;
|
||||
}
|
||||
return {
|
||||
schema: {
|
||||
name: STATISTICS_SCHEMA_NAME,
|
||||
version: STATISTICS_SCHEMA_VERSION
|
||||
},
|
||||
enabled: enabled,
|
||||
effective_enabled: effective_enabled,
|
||||
available: !!data.schema,
|
||||
collector_running: collector_running,
|
||||
volatile: to_bool(data.volatile, true),
|
||||
storage: sprintf('%s', data.storage || 'tmpfs'),
|
||||
persistent: to_bool(data.persistent, false),
|
||||
persistence_enabled: to_bool(data.persistence_enabled, false),
|
||||
persistence_healthy: to_bool(data.persistence_healthy, false),
|
||||
persistence_mode: sprintf('%s', data.persistence_mode || 'none'),
|
||||
persistent_error_count: to_int(data.persistent_error_count, 0),
|
||||
persistent_last_error_at: to_int(data.persistent_last_error_at, 0),
|
||||
persistent_updated_at: to_int(data.persistent_updated_at, 0),
|
||||
persistent_checkpoint_interval_s: to_int(data.persistent_checkpoint_interval_s, 3600),
|
||||
persistent_compacted_at: to_int(data.persistent_compacted_at, 0),
|
||||
persistent_compact_interval_s: to_int(data.persistent_compact_interval_s, 604800),
|
||||
snapshot_interval_s: snapshot_interval_s,
|
||||
effective_snapshot_interval_s: to_int(data.snapshot_interval_s, snapshot_interval_s),
|
||||
retention_hours: retention_hours,
|
||||
generation_id: sprintf('%s', data.generation_id || ''),
|
||||
snapshot_seq: to_int(data.snapshot_seq, 0),
|
||||
source: source,
|
||||
started_at: to_int(data.started_at, 0),
|
||||
session_started_at: to_int(data.session_started_at, 0),
|
||||
updated_at: to_int(data.updated_at, 0),
|
||||
totals: {
|
||||
queries: to_int(totals.queries, 0),
|
||||
blocked: to_int(totals.blocked, 0)
|
||||
},
|
||||
hourly: sanitize_hourly(data.hourly),
|
||||
device_limit: to_int(data.device_limit, 128),
|
||||
devices_truncated: to_bool(data.devices_truncated, false),
|
||||
devices: sanitize_devices(data.devices)
|
||||
};
|
||||
}
|
||||
return {
|
||||
build: build_statistics
|
||||
};
|
||||
@@ -0,0 +1,364 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
|
||||
let PKG_NAME = core.PKG_NAME;
|
||||
let PKG_VERSION = core.PKG_VERSION;
|
||||
let STATUS_FILE = core.STATUS_FILE;
|
||||
let BLOCKLIST_FILE = core.BLOCKLIST_FILE;
|
||||
let LOCAL_ALLOWLIST_FILE = core.LOCAL_ALLOWLIST_FILE;
|
||||
let LOCAL_BLOCKLIST_FILE = core.LOCAL_BLOCKLIST_FILE;
|
||||
let SCHEMA_NAME = core.SCHEMA_NAME;
|
||||
let SCHEMA_VERSION = core.SCHEMA_VERSION;
|
||||
let reload_uci = core.reload_uci;
|
||||
let cfg = core.cfg;
|
||||
let identity_cfg = core.identity_cfg;
|
||||
let read_json_file = core.read_json_file;
|
||||
let file_exists = core.file_exists;
|
||||
let file_size_kb = core.file_size_kb;
|
||||
let to_bool = core.to_bool;
|
||||
let to_optional_bool = core.to_optional_bool;
|
||||
let to_int = core.to_int;
|
||||
let mask_secret = core.mask_secret;
|
||||
let service_running = runtime.service_running;
|
||||
let dnsmasq_running = runtime.dnsmasq_running;
|
||||
|
||||
function build_api_source(data, enabled) {
|
||||
let api_resolve_ok = to_optional_bool(data.health_api_resolve);
|
||||
let artifact_download_ok = to_optional_bool(data.health_artifact_download);
|
||||
let last_result = data.last_result || '';
|
||||
let last_error_code = data.last_error_code || '';
|
||||
|
||||
if (!enabled) {
|
||||
last_result = 'disabled';
|
||||
}
|
||||
else if (api_resolve_ok == true && artifact_download_ok == true) {
|
||||
last_result = 'ok';
|
||||
}
|
||||
else if (api_resolve_ok == false) {
|
||||
last_result = last_error_code == 'safeshield_upgrade_required' ? last_error_code : 'api_resolve_failed';
|
||||
}
|
||||
else if (artifact_download_ok == false) {
|
||||
last_result = last_error_code == 'safeshield_upgrade_required' ? last_error_code : 'artifact_download_failed';
|
||||
}
|
||||
else if (!last_result || last_result == 'idle') {
|
||||
last_result = 'configured';
|
||||
}
|
||||
|
||||
return {
|
||||
section: 'hub_api',
|
||||
name: 'SmartSafeHub API Artifacts',
|
||||
action: 'block',
|
||||
enabled: enabled,
|
||||
last_result: last_result,
|
||||
line_count: to_int(data.artifact_unique_domains || data.valid_line_count || 0, 0),
|
||||
size_kb: to_int(data.blocklist_file_size_kb || 0, 0),
|
||||
artifact_tier: data.artifact_tier || '',
|
||||
artifact_version: data.artifact_version || '',
|
||||
source_count: to_int(data.artifact_source_count || (to_bool(data.artifact_download_url_present || '0', false) ? 1 : 0), 0),
|
||||
block_source_count: to_int(data.artifact_block_source_count || (to_bool(data.artifact_download_url_present || '0', false) ? 1 : 0), 0),
|
||||
allow_source_count: to_int(data.artifact_allow_source_count || 0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
function first_error_code(errors) {
|
||||
if (type(errors) != 'array' || length(errors) == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let e = errors[0];
|
||||
if (type(e) == 'object' && e.code != null) {
|
||||
return sprintf('%s', e.code);
|
||||
}
|
||||
|
||||
return sprintf('%s', e);
|
||||
}
|
||||
|
||||
function build_summary(status, stage, valid_line_count, artifact_tier, artifact_version, errors, warnings) {
|
||||
let sev = 'info';
|
||||
let label = 'Idle';
|
||||
let msg = 'SafeShield is idle';
|
||||
|
||||
if (status == 'ready') {
|
||||
label = 'Ready';
|
||||
|
||||
if (artifact_tier || artifact_version) {
|
||||
msg = sprintf('Hub artifact %s/%s is active, %d rules active', artifact_tier || 'unknown', artifact_version || 'unknown', valid_line_count);
|
||||
}
|
||||
else {
|
||||
msg = sprintf('Hub artifact is active, %d rules active', valid_line_count);
|
||||
}
|
||||
}
|
||||
else if (status == 'running') {
|
||||
sev = 'notice';
|
||||
label = 'Running';
|
||||
msg = stage ? sprintf('Refresh in progress (%s)', stage) : 'Refresh in progress';
|
||||
}
|
||||
else if (status == 'error') {
|
||||
sev = 'error';
|
||||
label = 'Error';
|
||||
msg = first_error_code(errors) || 'Refresh failed';
|
||||
}
|
||||
else if (status == 'paused') {
|
||||
sev = 'warning';
|
||||
label = 'Paused';
|
||||
msg = 'SafeShield is paused';
|
||||
}
|
||||
else if (status == 'disabled') {
|
||||
sev = 'warning';
|
||||
label = 'Disabled';
|
||||
msg = 'SafeShield is disabled';
|
||||
}
|
||||
|
||||
if (type(warnings) == 'array' && length(warnings) > 0 && sev == 'info') {
|
||||
sev = 'warning';
|
||||
}
|
||||
|
||||
return {
|
||||
label: label,
|
||||
message: msg,
|
||||
severity: sev
|
||||
};
|
||||
}
|
||||
|
||||
function build_status() {
|
||||
reload_uci();
|
||||
|
||||
let state = read_json_file(STATUS_FILE, {});
|
||||
let data = state.data || {};
|
||||
|
||||
let enabled = to_bool(cfg('enabled', '0'), false);
|
||||
let refresh_on_boot = to_bool(cfg('refresh_on_boot', '1'), true);
|
||||
let require_wan = to_bool(cfg('require_wan', '1'), true);
|
||||
let refresh_interval_s = to_int(cfg('refresh_interval_s', '28800'), 28800);
|
||||
let boot_start_delay_s = to_int(cfg('boot_start_delay_s', '30'), 30);
|
||||
|
||||
let license_key = cfg('license_key', '');
|
||||
let license_configured = !!license_key;
|
||||
let apply_local_overrides = to_bool(cfg('apply_local_overrides', '1'), true);
|
||||
|
||||
let cfg_physical_fingerprint = identity_cfg('physical_fingerprint', '');
|
||||
let cfg_fingerprint_version = identity_cfg('fingerprint_version', '1');
|
||||
let cfg_identity_provider = identity_cfg('identity_provider', '');
|
||||
let cfg_identity_source = identity_cfg('identity_source', '');
|
||||
let cfg_identity_strength = identity_cfg('identity_strength', '');
|
||||
let cfg_identity_profile = identity_cfg('identity_profile', '');
|
||||
let cfg_device_code = identity_cfg('device_code', '');
|
||||
let cfg_device_code_source = identity_cfg('device_code_source', 'unknown');
|
||||
let cfg_installation_id = identity_cfg('installation_id', '');
|
||||
let cfg_device_vendor = cfg('device_vendor', '');
|
||||
let cfg_device_model = cfg('device_model', '');
|
||||
let cfg_device_arch = cfg('device_arch', '');
|
||||
let cfg_device_memory_mb = cfg('device_memory_mb', '');
|
||||
|
||||
let status = data.status || (enabled ? 'idle' : 'disabled');
|
||||
let stage = data.stage || '';
|
||||
let valid_line_count = to_int(data.valid_line_count || 0, 0);
|
||||
let warnings = (type(state.warnings) == 'array') ? state.warnings : [];
|
||||
let errors = (type(state.errors) == 'array') ? state.errors : [];
|
||||
|
||||
let last_attempt = to_int(data.last_attempt || 0, 0);
|
||||
let last_success = to_int(data.last_success || 0, 0);
|
||||
let last_failure = to_int(data.last_failure || 0, 0);
|
||||
let last_local_apply = to_int(data.last_local_apply || 0, 0);
|
||||
let last_local_apply_failure = to_int(data.last_local_apply_failure || 0, 0);
|
||||
let next_refresh_at = to_int(data.next_refresh_at || 0, 0);
|
||||
let last_result = data.last_result || '';
|
||||
let last_error_code = data.last_error_code || '';
|
||||
|
||||
let blocklist_file_size_kb = to_int(data.blocklist_file_size_kb || 0, 0);
|
||||
let blocklist_installed = to_bool(data.blocklist_installed || '0', false);
|
||||
let blocklist_verification_ok = to_bool(data.blocklist_verification_ok || '0', false);
|
||||
let blocklist_test_domain = data.blocklist_test_domain || '';
|
||||
let blocklist_test_domain_sample_count = to_int(data.blocklist_test_domain_sample_count || 0, 0);
|
||||
let blocklist_test_domain_success_count = to_int(data.blocklist_test_domain_success_count || 0, 0);
|
||||
let blocklist_test_domains = data.blocklist_test_domains || '';
|
||||
let blocklist_backup_available = to_bool(data.blocklist_backup_available || '0', false);
|
||||
|
||||
let artifact_tier = data.artifact_tier || '';
|
||||
let artifact_version = data.artifact_version || '';
|
||||
let artifact_sha256 = data.artifact_sha256 || '';
|
||||
let artifact_unique_domains = to_int(data.artifact_unique_domains || 0, 0);
|
||||
let artifact_rules = to_int(data.artifact_rules || 0, 0);
|
||||
let artifact_download_url_present = to_bool(data.artifact_download_url_present || '0', false);
|
||||
let artifact_source_count = to_int(data.artifact_source_count || (artifact_download_url_present ? 1 : 0), 0);
|
||||
let artifact_block_source_count = to_int(data.artifact_block_source_count || (artifact_download_url_present ? 1 : 0), 0);
|
||||
let artifact_allow_source_count = to_int(data.artifact_allow_source_count || 0, 0);
|
||||
|
||||
let license_plan = data.license_plan || '';
|
||||
let license_status = data.license_status || '';
|
||||
let physical_fingerprint = data.physical_fingerprint || cfg_physical_fingerprint || '';
|
||||
let fingerprint_version = to_int(data.fingerprint_version || cfg_fingerprint_version || 1, 1);
|
||||
let identity_provider = data.identity_provider || cfg_identity_provider || '';
|
||||
let identity_source = data.identity_source || cfg_identity_source || '';
|
||||
let identity_strength = data.identity_strength || cfg_identity_strength || '';
|
||||
let identity_profile = data.identity_profile || cfg_identity_profile || '';
|
||||
let device_code = data.device_code || cfg_device_code || '';
|
||||
let device_code_source = data.device_code_source || cfg_device_code_source || 'unknown';
|
||||
let installation_id = data.installation_id || cfg_installation_id || '';
|
||||
let device_profile = data.device_profile || '';
|
||||
let dnsmasq_version = data.dnsmasq_version || '';
|
||||
let dnsmasq_min_version = data.dnsmasq_min_version || '2.93';
|
||||
|
||||
let health_dnsmasq_binary = to_bool(data.health_dnsmasq_binary || '0', false);
|
||||
let health_dnsmasq_version = to_bool(data.health_dnsmasq_version || '0', false);
|
||||
let health_dnsmasq_features = to_bool(data.health_dnsmasq_features || '0', false);
|
||||
let health_dnsmasq_confdir = to_bool(data.health_dnsmasq_confdir || '0', false);
|
||||
let health_dnsmasq_initial_restart = to_optional_bool(data.health_dnsmasq_initial_restart);
|
||||
let health_dnsmasq_final_restart = to_bool(data.health_dnsmasq_final_restart || '0', false);
|
||||
let health_dns_runtime = to_bool(data.health_dns_runtime || '0', false);
|
||||
let health_blocklist_verify = to_bool(data.health_blocklist_verify || '0', false);
|
||||
let health_min_valid_line_count = to_bool(data.health_min_valid_line_count || '0', false);
|
||||
let health_max_file_size = to_bool(data.health_max_file_size || '0', false);
|
||||
let health_api_resolve = to_optional_bool(data.health_api_resolve);
|
||||
let health_artifact_download = to_optional_bool(data.health_artifact_download);
|
||||
let health_artifact_sha256 = to_optional_bool(data.health_artifact_sha256);
|
||||
|
||||
let hub_source = build_api_source(data, enabled);
|
||||
let installed = blocklist_installed || file_exists(BLOCKLIST_FILE);
|
||||
let refreshd_running = service_running(PKG_NAME);
|
||||
let dns_running = dnsmasq_running();
|
||||
|
||||
let generated_at = time();
|
||||
let next_refresh_in_s = 0;
|
||||
|
||||
if (next_refresh_at > generated_at) {
|
||||
next_refresh_in_s = next_refresh_at - generated_at;
|
||||
}
|
||||
|
||||
return {
|
||||
schema: {
|
||||
name: SCHEMA_NAME,
|
||||
version: SCHEMA_VERSION
|
||||
},
|
||||
|
||||
name: PKG_NAME,
|
||||
version: PKG_VERSION,
|
||||
|
||||
enabled: enabled,
|
||||
active: refreshd_running,
|
||||
status: status,
|
||||
stage: stage,
|
||||
|
||||
summary: build_summary(status, stage, valid_line_count, artifact_tier, artifact_version, errors, warnings),
|
||||
|
||||
runtime: {
|
||||
refreshd_running: refreshd_running,
|
||||
dnsmasq_running: dns_running,
|
||||
dnsmasq_version: dnsmasq_version,
|
||||
dnsmasq_min_version: dnsmasq_min_version,
|
||||
dns_runtime_ok: health_dns_runtime || dns_running,
|
||||
config_loaded: true,
|
||||
require_wan: require_wan,
|
||||
refresh_on_boot: refresh_on_boot,
|
||||
last_result: last_result,
|
||||
last_error_code: last_error_code
|
||||
},
|
||||
license: {
|
||||
configured: license_configured,
|
||||
key_masked: mask_secret(license_key),
|
||||
plan: license_plan,
|
||||
status: license_status
|
||||
},
|
||||
|
||||
device: {
|
||||
physical_fingerprint: physical_fingerprint,
|
||||
fingerprint_version: fingerprint_version,
|
||||
identity_provider: identity_provider,
|
||||
identity_source: identity_source,
|
||||
identity_strength: identity_strength,
|
||||
identity_profile: identity_profile,
|
||||
device_code: device_code,
|
||||
device_code_source: device_code_source,
|
||||
installation_id: installation_id,
|
||||
profile: device_profile,
|
||||
configured: {
|
||||
physical_fingerprint: cfg_physical_fingerprint,
|
||||
vendor: cfg_device_vendor,
|
||||
model: cfg_device_model,
|
||||
arch: cfg_device_arch,
|
||||
memory_mb: to_int(cfg_device_memory_mb, 0)
|
||||
}
|
||||
},
|
||||
|
||||
artifact: {
|
||||
resolved: !!artifact_tier || !!artifact_version || artifact_download_url_present,
|
||||
tier: artifact_tier,
|
||||
version: artifact_version,
|
||||
sha256: artifact_sha256,
|
||||
unique_domains: artifact_unique_domains,
|
||||
rules: artifact_rules,
|
||||
download_url_present: artifact_download_url_present,
|
||||
source_count: artifact_source_count,
|
||||
block_source_count: artifact_block_source_count,
|
||||
allow_source_count: artifact_allow_source_count
|
||||
},
|
||||
|
||||
local_overrides: {
|
||||
enabled: apply_local_overrides,
|
||||
allowlist_path: LOCAL_ALLOWLIST_FILE,
|
||||
blocklist_path: LOCAL_BLOCKLIST_FILE
|
||||
},
|
||||
|
||||
blocklist: {
|
||||
installed: installed,
|
||||
path: BLOCKLIST_FILE,
|
||||
valid_line_count: valid_line_count,
|
||||
file_size_kb: blocklist_file_size_kb || file_size_kb(BLOCKLIST_FILE),
|
||||
verification_ok: blocklist_verification_ok,
|
||||
test_domain: blocklist_test_domain,
|
||||
test_domain_sample_count: blocklist_test_domain_sample_count,
|
||||
test_domain_success_count: blocklist_test_domain_success_count,
|
||||
test_domains: blocklist_test_domains,
|
||||
compressed: false,
|
||||
previous_backup_available: blocklist_backup_available
|
||||
},
|
||||
|
||||
sources: {
|
||||
mode: 'hub_api',
|
||||
total: 1,
|
||||
enabled: hub_source.enabled ? 1 : 0,
|
||||
items: [ hub_source ]
|
||||
},
|
||||
|
||||
health: {
|
||||
overall: (status == 'error') ? 'error' : ((type(warnings) == 'array' && length(warnings) > 0) ? 'warning' : 'ok'),
|
||||
checks: {
|
||||
api_resolve: health_api_resolve,
|
||||
artifact_download: health_artifact_download,
|
||||
artifact_sha256: health_artifact_sha256,
|
||||
dnsmasq_binary: health_dnsmasq_binary,
|
||||
dnsmasq_version: health_dnsmasq_version,
|
||||
dnsmasq_features: health_dnsmasq_features,
|
||||
dnsmasq_confdir: health_dnsmasq_confdir,
|
||||
dnsmasq_initial_restart: health_dnsmasq_initial_restart,
|
||||
dnsmasq_final_restart: health_dnsmasq_final_restart,
|
||||
dns_runtime: health_dns_runtime,
|
||||
blocklist_verify: health_blocklist_verify,
|
||||
min_valid_line_count: health_min_valid_line_count,
|
||||
max_file_size: health_max_file_size
|
||||
}
|
||||
},
|
||||
|
||||
warnings: warnings,
|
||||
errors: errors,
|
||||
|
||||
timestamps: {
|
||||
generated_at: generated_at,
|
||||
last_attempt: last_attempt,
|
||||
last_success: last_success,
|
||||
last_failure: last_failure,
|
||||
last_local_apply: last_local_apply,
|
||||
last_local_apply_failure: last_local_apply_failure,
|
||||
next_refresh_at: next_refresh_at,
|
||||
refresh_interval_s: refresh_interval_s,
|
||||
next_refresh_in_s: next_refresh_in_s,
|
||||
boot_start_delay_s: boot_start_delay_s
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
build: build_status
|
||||
};
|
||||
Reference in new issue
Block a user