mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
This commit is contained in:
1 parent
c7bafc635d
commit
c71da84035
264 files changed
+8169
-3944
No files matched your search
@@ -1,5 +1,83 @@
|
||||
# Changelog
|
||||
|
||||
## [0.3.25-r1] - 2026-10-10
|
||||
|
||||
- Bump version for release.
|
||||
|
||||
## [0.3.24-r6] - 2026-10-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- Distinguish a stopped DNS protection service from a temporarily unavailable status check without changing existing running flags.
|
||||
|
||||
### Improved
|
||||
|
||||
- Reuse successful procd service lookup results for up to one second to reduce repeated RPC work; retry failed lookups on the next request.
|
||||
|
||||
### Tests
|
||||
|
||||
- Cover unknown service states, retry after lookup failure, and short-lived lookup reuse.
|
||||
|
||||
## [0.3.24-r5] - 2026-10-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent SafeShield status and statistics RPC requests from blocking rpcd on nested service lookups.
|
||||
- Limit service status checks to two seconds and return a safe stopped state when procd cannot respond.
|
||||
|
||||
### Tests
|
||||
|
||||
- Cover isolated, time-bounded service lookups and timeout fallback behavior.
|
||||
|
||||
## [0.3.24-r4] - 2026-10-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename the active Hub plan/status runtime fields from `license_*` to `entitlement_*` and expose them through `safeshield.status.entitlement`.
|
||||
- Keep subscription entitlement separate from the retired local SafeShield license-key concept so downstream consumers no longer need license-shaped status fields.
|
||||
|
||||
### Tests
|
||||
|
||||
- Update ucode and ShellSpec contracts for the `entitlement` status schema and prevent the retired `license` status object from returning.
|
||||
|
||||
## [0.3.24-r3] - 2026-10-08
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the legacy SafeShield license-key RPC, UCI runtime handling, and masked/raw key exposure. Device/account authentication is owned by SmartSafeHub.
|
||||
- Remove the retired `license.uc` rpcd module and its host-side ucode tests.
|
||||
|
||||
### Changed
|
||||
|
||||
- Drive Cloud statistics eligibility exclusively from authenticated `/api/v1/devices/sync` responses instead of the presence of a local license key.
|
||||
- Recheck a denied statistics entitlement on the normal 12-hour cadence so account or subscription changes can recover without a legacy key transition.
|
||||
- Remove preserved legacy `safeshield.config.license_key` values during package upgrade or first boot.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Keep the init-script runtime version synchronized with package version `0.3.24-r3`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add regression contracts that reject legacy license-key RPC/config paths and verify statistics entitlement recovery without `ss_license_key`.
|
||||
|
||||
## [0.3.24-r2] - 2026-10-03
|
||||
|
||||
### Added
|
||||
|
||||
- Support authenticated Hub requests with the SmartSafeHub device credential managed by the device component.
|
||||
|
||||
### Changed
|
||||
|
||||
- Resolve protection data through `/api/v1/devices/sync` instead of the license-specific resolve endpoint.
|
||||
- Stop sending a license key when resolving protection data; account ownership and subscription entitlement are resolved by the Hub.
|
||||
- Wait for the SmartSafeHub device credential when device registration is still pending; keep the router, DNS service, and any previously applied protection data available while retrying later.
|
||||
- Refresh statistics upload credentials through `/api/v1/devices/sync` instead of the legacy license resolve API.
|
||||
|
||||
### Tests
|
||||
|
||||
- Added contract coverage for device-credential authentication, generic device sync, and removal of direct legacy license resolve calls from SafeShield.
|
||||
|
||||
## [0.3.24-r1] - 2026-09-19
|
||||
|
||||
- Bump version for release.
|
||||
|
||||
+7
-2
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=safeshield
|
||||
PKG_VERSION:=0.3.24
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=0.3.25
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
@@ -73,6 +73,7 @@ define Package/safeshield/install
|
||||
$(INSTALL_CONF) ./files/etc/config/safeshield $(1)/etc/config/safeshield
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/90_safeshield_identity $(1)/etc/uci-defaults/90_safeshield_identity
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/91_safeshield_remove_legacy_license $(1)/etc/uci-defaults/91_safeshield_remove_legacy_license
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/lib/safeshield
|
||||
$(INSTALL_BIN) ./files/usr/lib/safeshield/core.sh $(1)/usr/lib/safeshield/core.sh
|
||||
@@ -110,6 +111,10 @@ export IPKG_INSTROOT
|
||||
PKG_NAME='safeshield'
|
||||
export PKG_NAME
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
if [ -x /etc/uci-defaults/91_safeshield_remove_legacy_license ]; then
|
||||
/etc/uci-defaults/91_safeshield_remove_legacy_license >/dev/null 2>&1 || true
|
||||
rm -f /etc/uci-defaults/91_safeshield_remove_legacy_license
|
||||
fi
|
||||
mkdir -p /etc/safeshield
|
||||
if [ -r /usr/lib/safeshield/identity.sh ]; then
|
||||
(
|
||||
|
||||
+29
-50
@@ -380,7 +380,6 @@ SafeShield keeps the rpcd registration entrypoint intentionally small and loads
|
||||
├── config.uc
|
||||
├── refresh.uc
|
||||
├── rules.uc
|
||||
├── license.uc
|
||||
└── statistics.uc
|
||||
```
|
||||
|
||||
@@ -406,8 +405,6 @@ safeshield.refresh
|
||||
safeshield.rules_list
|
||||
safeshield.rule_add
|
||||
safeshield.rule_delete
|
||||
safeshield.license_get
|
||||
safeshield.license_update
|
||||
```
|
||||
|
||||
Read the current public configuration:
|
||||
@@ -463,36 +460,30 @@ unconsumed values instead of reusing a sequence already observed by Hub. This
|
||||
distinction lets cloud ingestion reject duplicate or out-of-order snapshots
|
||||
without treating a process restart as a new dataset.
|
||||
|
||||
When statistics are enabled, a separate uploader can synchronize the aggregate
|
||||
statistics with the SmartSafeHub Hub at `/api/v1/statistics`. Cloud upload is an
|
||||
entitlement-controlled feature: the Hub currently grants it to eligible PRO and
|
||||
ULTIMATE licenses in ACTIVE or TRIAL status and returns `statistics: null` for
|
||||
free, unlicensed, expired, or revoked devices. SafeShield treats that Hub response as the
|
||||
authoritative entitlement, disables network upload when it is absent, and keeps
|
||||
collecting local statistics normally. A device with no configured license key is
|
||||
blocked locally without making a statistics credential request.
|
||||
When statistics are enabled, a separate uploader can synchronize aggregate
|
||||
statistics with the SmartSafeHub Hub at `/api/v1/statistics`. Cloud upload is
|
||||
controlled entirely by the Hub response from `/api/v1/devices/sync`. SafeShield
|
||||
authenticates that sync with the SmartSafeHub device credential; it does not use
|
||||
or require a local license key. If the Hub omits statistics credentials, network
|
||||
upload is disabled while local statistics collection continues normally.
|
||||
|
||||
For an entitled device, the uploader gets a device-scoped bearer credential from
|
||||
the existing `/api/v1/licenses/resolve` response and keeps that credential only
|
||||
under `/tmp/safeshield/statistics/`; it is never written to flash or included in
|
||||
status output. Routine uploads run every 30 minutes and contain a consistent
|
||||
two-hour projection of the local hourly data. A full retained snapshot is sent
|
||||
on initial synchronization, after a failed upload has recovered, and periodically
|
||||
(approximately every 12 hours) to reconcile any missed window.
|
||||
For an entitled device, the uploader stores the short-lived statistics bearer
|
||||
credential only under `/tmp/safeshield/statistics/`; it is never written to flash
|
||||
or included in status output. Routine uploads run every 30 minutes and contain a
|
||||
consistent two-hour projection of the local hourly data. A full retained snapshot
|
||||
is sent on initial synchronization, after a failed upload has recovered, and
|
||||
periodically (approximately every 12 hours) to reconcile any missed window.
|
||||
|
||||
The uploader preserves the exact pending JSON across network retries. If the Hub
|
||||
committed a request but the HTTP acknowledgement was lost, retrying the same
|
||||
`generation_id` and `snapshot_seq` is therefore idempotent. An HTTP 401 clears
|
||||
the cached credential and resolves the license again. If the refreshed response
|
||||
no longer grants statistics upload, SafeShield discards the cloud pending payload
|
||||
and stops statistics POST requests. While a license key remains configured, a
|
||||
denied entitlement is rechecked at most once every 12 hours so a server-side
|
||||
reactivation can recover without waiting for the normal artifact refresh cycle.
|
||||
Changing or clearing the key through `license_update` immediately clears cached
|
||||
entitlement, credentials, pending payloads, and upload progress; the next granted
|
||||
entitlement therefore resumes with a full reconciliation. Local collection is
|
||||
independent of upload entitlement and upload success, so a free license, WAN
|
||||
outage, or Hub outage never interrupts on-router statistics.
|
||||
the cached statistics credential and performs device sync once before retrying.
|
||||
If the refreshed response no longer grants statistics upload, SafeShield discards
|
||||
the cloud pending payload and stops statistics POST requests. A denied entitlement
|
||||
is rechecked at most once every 12 hours, independent of plan or prior license-key
|
||||
state, so account/subscription changes can recover automatically. Local collection
|
||||
remains independent of upload entitlement and upload success, so WAN or Hub
|
||||
outages never interrupt on-router statistics.
|
||||
|
||||
The collector implementation is split into focused AWK modules under
|
||||
`/usr/lib/safeshield/statistics/` for common helpers, recovery, client identity,
|
||||
@@ -543,8 +534,9 @@ without installing an unsupported directive. The statistics source also exposes
|
||||
dnsmasq's `instance_id`, `transport_scope`, client-table capacity,
|
||||
tracked-client count, `untracked_queries`, and `untracked_blocked`. The OpenWrt 25.12 dnsmasq integration reports `transport_scope=udp+tcp`, so both UDP and TCP DNS requests are included in the cumulative SafeShield counters.
|
||||
|
||||
Update writable configuration values. `enabled` and `license_key` are
|
||||
intentionally excluded and have dedicated methods:
|
||||
Update writable configuration values. `enabled` is intentionally excluded and
|
||||
has a dedicated method. Device/account authentication is managed by SmartSafeHub
|
||||
and is not writable through the SafeShield RPC API:
|
||||
|
||||
```sh
|
||||
ubus call safeshield config_update '{
|
||||
@@ -621,28 +613,15 @@ ubus call safeshield rule_delete '{
|
||||
|
||||
`rule_add` and `rule_delete` keep the public SafeShield API unchanged, but their automatic apply path no longer performs a full Hub artifact refresh. SafeShield retains the normalized Hub domains in `/tmp/safeshield/api.block.txt`, rebuilds only the local allow/block inputs, merges them atomically, restarts dnsmasq, and verifies runtime DNS. The local apply worker shares the normal refresh lock, so a rule edit made during a full refresh waits for that refresh and then reapplies the newest local state. If the cached Hub artifact is missing, SafeShield falls back to one normal full refresh.
|
||||
|
||||
Read the raw license key only when an authenticated management client explicitly
|
||||
requests it. Normal `status`, `config` and `license_update` responses continue to
|
||||
return only masked license metadata.
|
||||
|
||||
```sh
|
||||
ubus call safeshield license_get
|
||||
```
|
||||
|
||||
Update or clear the license key. Passing an empty string removes the configured
|
||||
UCI option and immediately requests a SafeShield refresh so the device is resolved
|
||||
as unlicensed/free again. SafeShield intentionally treats an absent `license_key`
|
||||
option as the canonical unlicensed state; all runtime readers fall back to an empty
|
||||
key when the option is not present.
|
||||
|
||||
```sh
|
||||
ubus call safeshield license_update '{"license_key":"YOUR-LICENSE-KEY"}'
|
||||
ubus call safeshield license_update '{"license_key":""}'
|
||||
```
|
||||
SafeShield does not store or manage a user-entered license key. SmartSafeHub owns
|
||||
the device credential and the Hub resolves the current account/subscription
|
||||
entitlement during `/api/v1/devices/sync`. SafeShield status may expose the
|
||||
resolved plan/status for presentation, but never exposes or accepts account
|
||||
credentials.
|
||||
|
||||
The package also installs an rpcd ACL named `safeshield`. Read access covers
|
||||
`status`, `config` and `rules_list`; sensitive license access and mutating
|
||||
methods are declared as write access.
|
||||
`status`, `config` and `rules_list`; configuration, refresh, and local-rule
|
||||
mutations are declared as write access.
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ IPKG_INSTROOT="${IPKG_INSTROOT:-}"
|
||||
export IPKG_INSTROOT
|
||||
|
||||
readonly PKG_NAME='safeshield'
|
||||
readonly PKG_VERSION='0.3.24-r1'
|
||||
readonly PKG_VERSION='0.3.25-r1'
|
||||
|
||||
if type extra_command >/dev/null 2>&1; then
|
||||
extra_command 'apply_local_rules' 'Apply local allow/block rules using the cached Hub artifact'
|
||||
@@ -232,7 +232,7 @@ service_triggers() {
|
||||
status_service() {
|
||||
local status stage valid_line_count
|
||||
local enabled refresh_interval_s refresh_on_boot require_wan boot_delay
|
||||
local license_key artifact_tier artifact_version license_plan device_profile
|
||||
local artifact_tier artifact_version entitlement_plan device_profile
|
||||
local artifact_source_count artifact_block_source_count artifact_allow_source_count
|
||||
local dnsmasq_version dnsmasq_min_version
|
||||
local statistics_enabled statistics_snapshot_interval_s statistics_retention_hours
|
||||
@@ -246,7 +246,6 @@ status_service() {
|
||||
refresh_on_boot="$(ss_config_get config refresh_on_boot 1)"
|
||||
require_wan="$(ss_config_get config require_wan 1)"
|
||||
boot_delay="$(ss_config_get config boot_start_delay_s 30)"
|
||||
license_key="$(ss_config_get config license_key '')"
|
||||
statistics_enabled="$(ss_config_get config statistics_enabled 1)"
|
||||
statistics_snapshot_interval_s="$(ss_config_get config statistics_snapshot_interval_s 60)"
|
||||
statistics_retention_hours="$(ss_config_get config statistics_retention_hours 168)"
|
||||
@@ -264,7 +263,7 @@ status_service() {
|
||||
artifact_source_count="$(json get artifact_source_count 2>/dev/null)"
|
||||
artifact_block_source_count="$(json get artifact_block_source_count 2>/dev/null)"
|
||||
artifact_allow_source_count="$(json get artifact_allow_source_count 2>/dev/null)"
|
||||
license_plan="$(json get license_plan 2>/dev/null)"
|
||||
entitlement_plan="$(json get entitlement_plan 2>/dev/null)"
|
||||
device_profile="$(json get device_profile 2>/dev/null)"
|
||||
dnsmasq_version="$(json get dnsmasq_version 2>/dev/null)"
|
||||
dnsmasq_min_version="$(json get dnsmasq_min_version 2>/dev/null)"
|
||||
@@ -278,7 +277,6 @@ status_service() {
|
||||
printf 'refresh_on_boot: %s\n' "$refresh_on_boot"
|
||||
printf 'require_wan: %s\n' "$require_wan"
|
||||
printf 'boot_start_delay_s: %s\n' "$boot_delay"
|
||||
printf 'license_key: %s\n' "$(ss_mask_secret "$license_key")"
|
||||
|
||||
if [ -f "${SS_BLOCKLIST_FILE}" ]; then
|
||||
printf '%s\n' 'blocklist: installed'
|
||||
@@ -296,7 +294,7 @@ status_service() {
|
||||
|
||||
printf 'status: %s\n' "$status"
|
||||
[ -n "$stage" ] && printf 'stage: %s\n' "$stage"
|
||||
[ -n "$license_plan" ] && printf 'license_plan: %s\n' "$license_plan"
|
||||
[ -n "$entitlement_plan" ] && printf 'entitlement_plan: %s\n' "$entitlement_plan"
|
||||
[ -n "$device_profile" ] && printf 'device_profile: %s\n' "$device_profile"
|
||||
[ -n "$artifact_tier" ] && printf 'artifact_tier: %s\n' "$artifact_tier"
|
||||
[ -n "$artifact_version" ] && printf 'artifact_version: %s\n' "$artifact_version"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# SafeShield authentication is device-scoped. Remove the retired raw license
|
||||
# key from preserved configurations so obsolete secrets do not remain on flash.
|
||||
if uci -q get safeshield.config.license_key >/dev/null 2>&1; then
|
||||
uci -q delete safeshield.config.license_key
|
||||
uci -q commit safeshield
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -177,6 +177,17 @@ ss_read_installed_version() {
|
||||
printf '%s' "$version"
|
||||
}
|
||||
|
||||
ss_device_credential_path() {
|
||||
printf '%s' "${SS_DEVICE_CREDENTIAL_FILE:-/etc/smartsafehub/device-credential.json}"
|
||||
}
|
||||
|
||||
ss_device_credential_token() {
|
||||
local path
|
||||
path="$(ss_device_credential_path)"
|
||||
[ -r "$path" ] || return 1
|
||||
ss_json_get_file "$path" '@.token'
|
||||
}
|
||||
|
||||
ss_write_resolve_payload() {
|
||||
local out="$1"
|
||||
local model vendor arch memory physical_fingerprint safeshield_version
|
||||
@@ -202,7 +213,6 @@ ss_write_resolve_payload() {
|
||||
|
||||
cat >"$out" <<__SAFESHIELD_JSON__
|
||||
{
|
||||
"license_key": $(ss_json_value "$ss_license_key"),
|
||||
"device": {
|
||||
"physical_fingerprint": $(ss_json_value "$physical_fingerprint"),
|
||||
"fingerprint_version": ${SS_FINGERPRINT_VERSION:-1},
|
||||
@@ -494,22 +504,25 @@ ss_resolve_artifact_sources() {
|
||||
}
|
||||
|
||||
ss_resolve_artifact() {
|
||||
local url response payload retries ok statistics_entitlement_rc
|
||||
local url response payload retries ok statistics_entitlement_rc device_credential
|
||||
|
||||
SS_RESOLVE_ERROR_CODE='api_resolve_failed'
|
||||
url='https://www.smartsafehub.com/api/v1/licenses/resolve'
|
||||
url='https://www.smartsafehub.com/api/v1/devices/sync'
|
||||
payload="$SS_API_PAYLOAD"
|
||||
response="$SS_API_RESPONSE"
|
||||
|
||||
ss_status_set artifact_download_url_present "0"
|
||||
|
||||
if [ -z "$ss_license_key" ]; then
|
||||
log_info "license_key is empty; resolving SafeShield artifact as unlicensed/free device"
|
||||
ss_status_set license_plan "free"
|
||||
ss_status_set license_status "unlicensed"
|
||||
fi
|
||||
|
||||
ss_write_resolve_payload "$payload" || return 1
|
||||
device_credential="$(ss_device_credential_token 2>/dev/null || true)"
|
||||
if [ -z "$device_credential" ]; then
|
||||
log_warn "SmartSafeHub device credential is not available yet; retrying on the next refresh"
|
||||
SS_RESOLVE_ERROR_CODE='device_registration_pending'
|
||||
ss_status_set device_registration_status 'pending'
|
||||
ss_status_set health_api_resolve "0"
|
||||
return 1
|
||||
fi
|
||||
ss_status_set device_registration_status 'registered'
|
||||
|
||||
retries=1
|
||||
ok=0
|
||||
@@ -517,7 +530,7 @@ ss_resolve_artifact() {
|
||||
ss_should_stop && return 130
|
||||
log_info "Resolving SafeShield artifact via Hub API (try ${retries}/${ss_download_retry})"
|
||||
|
||||
if ss_http_post_json "$url" "$payload" "$response" && [ -s "$response" ]; then
|
||||
if ss_http_post_json "$url" "$payload" "$response" "Device ${device_credential}" && [ -s "$response" ]; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
@@ -546,8 +559,8 @@ ss_resolve_artifact() {
|
||||
ss_resolved_artifact_version="$(ss_json_get_file "$response" '@.artifact.version')"
|
||||
ss_resolved_artifact_unique_domains="$(ss_json_get_file "$response" '@.artifact.unique_domains')"
|
||||
ss_resolved_artifact_rules="$(ss_json_get_file "$response" '@.artifact.rules')"
|
||||
ss_resolved_license_plan="$(ss_json_get_file "$response" '@.license.plan')"
|
||||
ss_resolved_license_status="$(ss_json_get_file "$response" '@.license.status')"
|
||||
ss_resolved_entitlement_plan="$(ss_json_get_file "$response" '@.entitlement.plan')"
|
||||
ss_resolved_entitlement_status="$(ss_json_get_file "$response" '@.entitlement.status')"
|
||||
ss_resolved_device_profile="$(ss_json_get_file "$response" '@.device.profile')"
|
||||
|
||||
if command -v ss_statistics_sync_upload_entitlement >/dev/null 2>&1; then
|
||||
@@ -555,7 +568,7 @@ ss_resolve_artifact() {
|
||||
ss_statistics_sync_upload_entitlement "$response" || statistics_entitlement_rc=$?
|
||||
case "$statistics_entitlement_rc" in
|
||||
0) ;;
|
||||
2) log_info "Cloud statistics upload is not enabled for the current license" ;;
|
||||
2) log_info "Cloud statistics upload is not enabled for the current plan" ;;
|
||||
*) log_warn "Hub API response did not include usable statistics upload credentials" ;;
|
||||
esac
|
||||
fi
|
||||
@@ -572,12 +585,12 @@ ss_resolve_artifact() {
|
||||
ss_status_set artifact_version "$ss_resolved_artifact_version"
|
||||
ss_status_set artifact_unique_domains "${ss_resolved_artifact_unique_domains:-0}"
|
||||
ss_status_set artifact_rules "${ss_resolved_artifact_rules:-0}"
|
||||
ss_status_set license_plan "${ss_resolved_license_plan:-free}"
|
||||
ss_status_set license_status "${ss_resolved_license_status:-unlicensed}"
|
||||
ss_status_set entitlement_plan "${ss_resolved_entitlement_plan:-free}"
|
||||
ss_status_set entitlement_status "${ss_resolved_entitlement_status:-unlicensed}"
|
||||
ss_status_set device_profile "$ss_resolved_device_profile"
|
||||
SS_RESOLVE_ERROR_CODE=''
|
||||
|
||||
log_ok "Resolved ${ss_resolved_artifact_tier:-unknown}/${ss_resolved_artifact_version:-unknown} artifact sources for plan ${ss_resolved_license_plan:-free}"
|
||||
log_ok "Resolved ${ss_resolved_artifact_tier:-unknown}/${ss_resolved_artifact_version:-unknown} artifact sources for plan ${ss_resolved_entitlement_plan:-free}"
|
||||
}
|
||||
|
||||
ss_verify_artifact_sha256() {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
ss_enabled="0"
|
||||
ss_verbosity="2"
|
||||
ss_license_key=""
|
||||
ss_device_vendor=""
|
||||
ss_device_model=""
|
||||
ss_device_arch=""
|
||||
@@ -105,7 +104,6 @@ ss_load_config() {
|
||||
|
||||
ss_enabled="$(ss_config_get config enabled 0)"
|
||||
ss_verbosity="$(ss_config_get config verbosity 2)"
|
||||
ss_license_key="$(ss_config_get config license_key '')"
|
||||
ss_device_vendor="$(ss_config_get config device_vendor '')"
|
||||
ss_device_model="$(ss_config_get config device_model '')"
|
||||
ss_device_arch="$(ss_config_get config device_arch '')"
|
||||
|
||||
@@ -358,20 +358,22 @@ ss_statistics_load_upload_credentials() {
|
||||
}
|
||||
|
||||
ss_statistics_refresh_upload_credentials() {
|
||||
local request response resolve_url retries rc=1
|
||||
local request response sync_url retries rc=1 device_credential
|
||||
|
||||
resolve_url='https://www.smartsafehub.com/api/v1/licenses/resolve'
|
||||
request="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-resolve-request.json"
|
||||
response="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-resolve-response.json"
|
||||
sync_url='https://www.smartsafehub.com/api/v1/devices/sync'
|
||||
request="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-sync-request.json"
|
||||
response="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-sync-response.json"
|
||||
|
||||
ss_load_config || return 1
|
||||
device_credential="$(ss_device_credential_token 2>/dev/null || true)"
|
||||
[ -n "$device_credential" ] || return 1
|
||||
mkdir -p "${request%/*}" || return 1
|
||||
ss_refresh_lock_open_wait "${ss_download_timeout:-10}" || return $?
|
||||
|
||||
if ! ss_write_resolve_payload "$request"; then
|
||||
printf '{}\n' >"$request" || {
|
||||
ss_refresh_lock_close
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
retries=1
|
||||
while [ "$retries" -le "${ss_download_retry:-3}" ]; do
|
||||
@@ -379,7 +381,7 @@ ss_statistics_refresh_upload_credentials() {
|
||||
rc=130
|
||||
break
|
||||
}
|
||||
if ss_http_post_json "$resolve_url" "$request" "$response" && [ -s "$response" ]; then
|
||||
if ss_http_post_json "$sync_url" "$request" "$response" "Device ${device_credential}" && [ -s "$response" ]; then
|
||||
rc=0
|
||||
ss_statistics_sync_upload_entitlement "$response" || rc=$?
|
||||
break
|
||||
|
||||
@@ -70,8 +70,8 @@ ss_status_reset_blocklist_fields() {
|
||||
}
|
||||
|
||||
ss_status_reset_artifact_fields() {
|
||||
ss_status_set license_plan ""
|
||||
ss_status_set license_status ""
|
||||
ss_status_set entitlement_plan ""
|
||||
ss_status_set entitlement_status ""
|
||||
ss_status_set physical_fingerprint ""
|
||||
ss_status_set fingerprint_version ""
|
||||
ss_status_set identity_provider ""
|
||||
|
||||
@@ -8,23 +8,6 @@ command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ss_mask_secret() {
|
||||
local value="$1"
|
||||
local len
|
||||
|
||||
[ -n "$value" ] || {
|
||||
printf '%s' ''
|
||||
return 0
|
||||
}
|
||||
|
||||
len="${#value}"
|
||||
if [ "$len" -le 8 ] 2>/dev/null; then
|
||||
printf '%s' '********'
|
||||
else
|
||||
printf '%s****%s' "${value%${value#????}}" "${value#${value%????}}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_valid_integer() {
|
||||
case "$1" in
|
||||
'' | *[!0-9]*) return 1 ;;
|
||||
|
||||
@@ -375,7 +375,7 @@ upload_send_pending() {
|
||||
continue
|
||||
fi
|
||||
if [ "$refresh_rc" -eq 2 ]; then
|
||||
log_info 'Cloud statistics upload is no longer enabled for the current license'
|
||||
log_info 'Cloud statistics upload is no longer enabled for the current account entitlement'
|
||||
upload_pending_clear
|
||||
return 2
|
||||
fi
|
||||
@@ -433,14 +433,11 @@ main() {
|
||||
fi
|
||||
|
||||
wait_s="$SS_STATS_UPLOAD_INTERVAL_S"
|
||||
if [ -z "${ss_license_key:-}" ]; then
|
||||
ss_statistics_upload_denied || ss_statistics_disable_cloud_upload >/dev/null 2>&1 || true
|
||||
fi
|
||||
if ss_statistics_upload_denied; then
|
||||
upload_pending_clear
|
||||
if [ -n "${ss_license_key:-}" ] && ss_statistics_upload_denied_recheck_due "$SS_STATS_UPLOAD_DENIED_RECHECK_S"; then
|
||||
if ss_statistics_upload_denied_recheck_due "$SS_STATS_UPLOAD_DENIED_RECHECK_S"; then
|
||||
log_info 'Rechecking Hub statistics upload entitlement'
|
||||
# Stamp this attempt before network I/O so a failed resolve cannot
|
||||
# Stamp this attempt before network I/O so a failed device sync cannot
|
||||
# turn into a routine upload retry loop while entitlement is denied.
|
||||
ss_statistics_set_upload_entitlement denied >/dev/null 2>&1 || true
|
||||
entitlement_rc=0
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
"set_enabled",
|
||||
"refresh",
|
||||
"rule_add",
|
||||
"rule_delete",
|
||||
"license_get",
|
||||
"license_update"
|
||||
"rule_delete"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 {
|
||||
@@ -66,17 +65,7 @@ return {
|
||||
refresh: true
|
||||
},
|
||||
call: rules.delete
|
||||
},
|
||||
|
||||
license_get: {
|
||||
call: license.get
|
||||
},
|
||||
|
||||
license_update: {
|
||||
args: {
|
||||
license_key: ''
|
||||
},
|
||||
call: license.update
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -12,7 +12,6 @@ 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;
|
||||
@@ -46,10 +45,6 @@ function build_config() {
|
||||
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', ''),
|
||||
@@ -67,9 +62,6 @@ function validate_config_value(name, value) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -149,20 +149,6 @@ function identity_cfg(name, def) {
|
||||
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 = {
|
||||
@@ -242,7 +228,6 @@ return {
|
||||
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,
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
'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
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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;
|
||||
@@ -12,43 +11,72 @@ 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;
|
||||
// Do not call ubus synchronously from an rpcd handler. In particular,
|
||||
// service.list may require the same event loop currently serving the caller.
|
||||
// Use an independent ubus client with its built-in -t timeout (seconds).
|
||||
// BusyBox/OpenWrt installations do not necessarily provide coreutils timeout.
|
||||
// Only successful procd lookups are briefly reused. A failed lookup must not
|
||||
// poison subsequent requests or be mistaken for a stopped service.
|
||||
let service_cache = {};
|
||||
|
||||
function service_status(name, use_cache) {
|
||||
if (name != PKG_NAME && name != 'dnsmasq') {
|
||||
return { state: 'unknown', running: false, lookup_ok: false };
|
||||
}
|
||||
|
||||
for (let inst_name, inst in r[name].instances) {
|
||||
if (inst.running) {
|
||||
return true;
|
||||
let now = time();
|
||||
let cached = service_cache[name];
|
||||
if (use_cache && cached && cached.at == now) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
let pipe = fs.popen(sprintf("ubus -t 2 call service list '{\"name\":\"%s\"}' 2>/dev/null", name), 'r');
|
||||
if (!pipe) {
|
||||
return { state: 'unknown', running: false, lookup_ok: false };
|
||||
}
|
||||
|
||||
let output = pipe.read('all');
|
||||
pipe.close();
|
||||
if (!output) {
|
||||
return { state: 'unknown', running: false, lookup_ok: false };
|
||||
}
|
||||
|
||||
let result = json(output);
|
||||
// procd returns an empty object when the named service is absent.
|
||||
// That is a valid stopped response, not a transport failure.
|
||||
if (type(result) != 'object') {
|
||||
return { state: 'unknown', running: false, lookup_ok: false };
|
||||
}
|
||||
|
||||
let instances = result[name] && result[name].instances;
|
||||
let running = false;
|
||||
if (type(instances) == 'object') {
|
||||
for (let instance_name, instance in instances) {
|
||||
if (instance && instance.running) {
|
||||
running = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
let value = { state: running ? 'running' : 'stopped', running: running, lookup_ok: true, instances: instances || {} };
|
||||
if (use_cache) {
|
||||
service_cache[name] = { at: now, value: value };
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function service_running(name) {
|
||||
return service_status(name, false).running;
|
||||
}
|
||||
|
||||
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;
|
||||
let state = service_status(name, false);
|
||||
return !!(state.instances && state.instances[instance_name] && state.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;
|
||||
return service_running('dnsmasq');
|
||||
}
|
||||
|
||||
function run_service_action(action, timeout_ms) {
|
||||
@@ -85,10 +113,11 @@ function start_refresh_async() {
|
||||
};
|
||||
}
|
||||
|
||||
if (!service_running(PKG_NAME)) {
|
||||
let service = service_status(PKG_NAME, false);
|
||||
if (!service.running) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'service_stopped'
|
||||
reason: service.lookup_ok ? 'service_stopped' : 'service_unavailable'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,10 +158,11 @@ function start_local_apply_async() {
|
||||
};
|
||||
}
|
||||
|
||||
if (!service_running(PKG_NAME)) {
|
||||
let service = service_status(PKG_NAME, false);
|
||||
if (!service.running) {
|
||||
return {
|
||||
accepted: false,
|
||||
reason: 'service_stopped'
|
||||
reason: service.lookup_ok ? 'service_stopped' : 'service_unavailable'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,6 +185,7 @@ function start_local_apply_async() {
|
||||
|
||||
return {
|
||||
service_running: service_running,
|
||||
service_status: service_status,
|
||||
service_instance_running: service_instance_running,
|
||||
dnsmasq_running: dnsmasq_running,
|
||||
run_service_action: run_service_action,
|
||||
|
||||
@@ -91,7 +91,8 @@ function build_statistics() {
|
||||
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 collector_service = runtime.service_status(PKG_NAME, true);
|
||||
let collector_running = !!(collector_service.instances && collector_service.instances.statistics && collector_service.instances.statistics.running);
|
||||
let effective_enabled = enabled && collector_running;
|
||||
let source = sanitize_source(data.source);
|
||||
if (!effective_enabled) {
|
||||
@@ -106,6 +107,8 @@ function build_statistics() {
|
||||
effective_enabled: effective_enabled,
|
||||
available: !!data.schema,
|
||||
collector_running: collector_running,
|
||||
collector_state: collector_service.lookup_ok ? (collector_running ? 'running' : 'stopped') : 'unknown',
|
||||
collector_lookup_ok: collector_service.lookup_ok,
|
||||
volatile: to_bool(data.volatile, true),
|
||||
storage: sprintf('%s', data.storage || 'tmpfs'),
|
||||
persistent: to_bool(data.persistent, false),
|
||||
|
||||
@@ -20,7 +20,6 @@ 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;
|
||||
|
||||
@@ -134,8 +133,6 @@ function build_status() {
|
||||
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', '');
|
||||
@@ -186,8 +183,8 @@ function build_status() {
|
||||
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 entitlement_plan = data.entitlement_plan || '';
|
||||
let entitlement_status = data.entitlement_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 || '';
|
||||
@@ -217,8 +214,10 @@ function build_status() {
|
||||
|
||||
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 refresh_service = runtime.service_status(PKG_NAME, true);
|
||||
let dns_service = runtime.service_status('dnsmasq', true);
|
||||
let refreshd_running = refresh_service.running;
|
||||
let dns_running = dns_service.running;
|
||||
|
||||
let generated_at = time();
|
||||
let next_refresh_in_s = 0;
|
||||
@@ -245,7 +244,11 @@ function build_status() {
|
||||
|
||||
runtime: {
|
||||
refreshd_running: refreshd_running,
|
||||
refreshd_state: refresh_service.state,
|
||||
refreshd_lookup_ok: refresh_service.lookup_ok,
|
||||
dnsmasq_running: dns_running,
|
||||
dnsmasq_state: dns_service.state,
|
||||
dnsmasq_lookup_ok: dns_service.lookup_ok,
|
||||
dnsmasq_version: dnsmasq_version,
|
||||
dnsmasq_min_version: dnsmasq_min_version,
|
||||
dns_runtime_ok: health_dns_runtime || dns_running,
|
||||
@@ -255,11 +258,9 @@ function build_status() {
|
||||
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
|
||||
entitlement: {
|
||||
plan: entitlement_plan,
|
||||
status: entitlement_status
|
||||
},
|
||||
|
||||
device: {
|
||||
|
||||
@@ -50,6 +50,13 @@ Describe 'SafeShield core shell behavior'
|
||||
The error should equal ''
|
||||
End
|
||||
|
||||
It 'removes preserved legacy license keys during upgrade'
|
||||
When call ss_case_legacy_license_cleanup
|
||||
The status should be success
|
||||
The output should equal ''
|
||||
The error should equal ''
|
||||
End
|
||||
|
||||
It 'keeps package and runtime versions synchronized'
|
||||
When call ss_case_status_version
|
||||
The status should be success
|
||||
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
# shellcheck shell=sh
|
||||
|
||||
Describe 'SafeShield device API integration'
|
||||
It 'uses the generic device sync endpoint with device authentication'
|
||||
When call ss_case_device_api_contract
|
||||
The status should be success
|
||||
The output should equal ''
|
||||
The error should equal ''
|
||||
End
|
||||
|
||||
It 'waits cleanly when the SmartSafeHub device credential is unavailable'
|
||||
When call ss_case_device_registration_pending
|
||||
The status should be success
|
||||
The output should equal ''
|
||||
The error should equal ''
|
||||
End
|
||||
End
|
||||
@@ -18,6 +18,8 @@ export SS_SPEC_ROOT
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/spec/support/blocklist_cases.sh"
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/spec/support/device_api_cases.sh"
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/spec/support/runtime_cases.sh"
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/spec/support/statistics_cases.sh"
|
||||
|
||||
@@ -150,7 +150,6 @@ ss_case_multi_artifact() (
|
||||
SS_RESOLVED_SOURCES="$SS_TMP_DIR/resolved-sources.tsv"
|
||||
SS_ARTIFACT_CACHE_STATE="$SS_TMP_DIR/artifact-sources.state"
|
||||
SS_MAX_ARTIFACT_SOURCES=16
|
||||
ss_license_key='test-license'
|
||||
ss_download_retry=1
|
||||
ss_download_timeout=10
|
||||
ss_max_blocklist_file_size_kb=1024
|
||||
@@ -159,11 +158,13 @@ ss_case_multi_artifact() (
|
||||
ss_status_set() { :; }
|
||||
ss_status_add_error() { :; }
|
||||
log_info() { :; }
|
||||
log_warn() { :; }
|
||||
log_error() { :; }
|
||||
log_ok() { :; }
|
||||
command_exists() { command -v "$1" >/dev/null 2>&1; }
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||
ss_device_credential_token() { printf '%s\n' 'test-device-credential'; }
|
||||
ss_write_resolve_payload() { printf '{}\n' >"$1"; }
|
||||
ss_http_post_json() { printf '{}\n' >"$3"; }
|
||||
ss_json_get_file() {
|
||||
@@ -184,8 +185,8 @@ ss_case_multi_artifact() (
|
||||
'@.artifact.tier') printf '%s\n' 'pro' ;;
|
||||
'@.artifact.version') printf '%s\n' '20260830T120000Z' ;;
|
||||
'@.artifact.unique_domains' | '@.artifact.rules') printf '%s\n' '4' ;;
|
||||
'@.license.plan') printf '%s\n' 'pro' ;;
|
||||
'@.license.status') printf '%s\n' 'active' ;;
|
||||
'@.entitlement.plan') printf '%s\n' 'pro' ;;
|
||||
'@.entitlement.status') printf '%s\n' 'active' ;;
|
||||
'@.device.profile') printf '%s\n' 'test-profile' ;;
|
||||
*) : ;;
|
||||
esac
|
||||
@@ -277,7 +278,6 @@ ss_case_upgrade_required() (
|
||||
SS_API_RESPONSE="$SS_TMP_DIR/resolve-response.json"
|
||||
SS_RESOLVED_SOURCES="$SS_TMP_DIR/resolved-sources.tsv"
|
||||
SS_ARTIFACT_CACHE_STATE="$SS_TMP_DIR/artifact-sources.state"
|
||||
ss_license_key='test-license'
|
||||
ss_download_retry=3
|
||||
ss_download_timeout=10
|
||||
ss_max_blocklist_file_size_kb=1024
|
||||
@@ -287,12 +287,14 @@ ss_case_upgrade_required() (
|
||||
ss_status_add_error() { :; }
|
||||
ss_status_add_warning() { :; }
|
||||
log_info() { :; }
|
||||
log_warn() { :; }
|
||||
log_error() { :; }
|
||||
log_ok() { :; }
|
||||
command_exists() { return 1; }
|
||||
sleep() { printf 'sleep\n' >>"$TMP_DIR/sleep"; }
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||
ss_device_credential_token() { printf '%s\n' 'test-device-credential'; }
|
||||
ss_write_resolve_payload() { printf '{}\n' >"$1"; }
|
||||
|
||||
printf '%s\n' 'HTTP error 426' >"$TMP_DIR/uclient.log"
|
||||
|
||||
@@ -10,9 +10,6 @@ ss_case_utils() (
|
||||
|
||||
command_exists sh
|
||||
! command_exists safeshield-command-that-does-not-exist
|
||||
ss_spec_assert_eq "$(ss_mask_secret '')" ''
|
||||
ss_spec_assert_eq "$(ss_mask_secret '12345678')" '********'
|
||||
ss_spec_assert_eq "$(ss_mask_secret 'abcd1234wxyz')" 'abcd****wxyz'
|
||||
is_valid_integer 0
|
||||
is_valid_integer 42
|
||||
for value in '' -1 1.5 12x; do
|
||||
@@ -303,7 +300,6 @@ ss_case_resolve_payload() (
|
||||
esac
|
||||
}
|
||||
ss_status_set() { :; }
|
||||
ss_license_key='test-license'
|
||||
SS_VERSION_FILE="$TMP_DIR/version"
|
||||
export SS_VERSION_FILE
|
||||
printf '%s\n' '0.3.18-r1' >"$SS_VERSION_FILE"
|
||||
@@ -311,7 +307,8 @@ ss_case_resolve_payload() (
|
||||
ss_write_resolve_payload "$payload"
|
||||
ss_spec_assert_file_contains "$payload" ' "safeshield_version": "0.3.18-r1"'
|
||||
! grep -Eq '^ "safeshield_version"' "$payload"
|
||||
ss_spec_assert_file_contains "$payload" '"license_key": "test-license"'
|
||||
! grep -Fq '"credential"' "$payload"
|
||||
! grep -Fq '"license_key"' "$payload"
|
||||
ss_spec_assert_file_contains "$payload" '"physical_fingerprint": "test-fingerprint"'
|
||||
ss_spec_assert_file_contains "$payload" '"fingerprint_version": 1'
|
||||
ss_spec_assert_file_contains "$payload" '"identity_provider": "factory_mac"'
|
||||
@@ -348,15 +345,20 @@ ss_case_rpcd_contract() (
|
||||
[ -f "$ENTRY" ]
|
||||
[ -f "$ACL" ]
|
||||
ss_spec_assert_file_contains "$ENTRY" "unshift(REQUIRE_SEARCH_PATH, '/usr/share/rpcd/ucode/safeshield/*.uc');"
|
||||
for module in status config refresh rules license statistics; do
|
||||
for module in status config refresh rules statistics; do
|
||||
ss_spec_assert_file_contains "$ENTRY" "let $module = require('$module');"
|
||||
done
|
||||
for method in status config statistics config_update set_enabled refresh rules_list rule_add rule_delete license_get license_update; do
|
||||
for method in status config statistics config_update set_enabled refresh rules_list rule_add rule_delete; do
|
||||
ss_spec_assert_file_contains "$ENTRY" " $method: {"
|
||||
ss_spec_assert_file_contains "$ACL" "\"$method\""
|
||||
done
|
||||
ss_spec_assert_file_contains "$MAKEFILE" './files/usr/share/rpcd/ucode/safeshield/*.uc'
|
||||
ss_spec_assert_file_contains "$MAKEFILE" './files/usr/share/rpcd/acl.d/safeshield.json'
|
||||
[ ! -e "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/license.uc" ]
|
||||
ss_spec_assert_file_not_contains "$ENTRY" 'license_get'
|
||||
ss_spec_assert_file_not_contains "$ENTRY" 'license_update'
|
||||
ss_spec_assert_file_not_contains "$ACL" 'license_get'
|
||||
ss_spec_assert_file_not_contains "$ACL" 'license_update'
|
||||
ss_spec_assert_file_contains "$STATISTICS_MODULE" 'effective_snapshot_interval_s: to_int(data.snapshot_interval_s, snapshot_interval_s)'
|
||||
)
|
||||
|
||||
@@ -413,7 +415,7 @@ ss_case_status_state() (
|
||||
done
|
||||
: >"$CALLS"
|
||||
ss_status_reset_artifact_fields
|
||||
for key in license_plan artifact_tier artifact_source_count artifact_allow_source_count; do
|
||||
for key in entitlement_plan artifact_tier artifact_source_count artifact_allow_source_count; do
|
||||
grep -F "$(printf 'set\t%s\t' "$key")" "$CALLS" >/dev/null
|
||||
done
|
||||
mkdir -p "$SS_DNSMASQ_DIR"
|
||||
@@ -429,6 +431,29 @@ ss_case_status_state() (
|
||||
ss_spec_assert_eq "$(cat "$SS_BLOCKLIST_FILE")" 'address=/ads.example/#'
|
||||
)
|
||||
|
||||
ss_case_legacy_license_cleanup() (
|
||||
set -eu
|
||||
TMP_DIR="$(ss_spec_tmpdir)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
||||
mkdir -p "$TMP_DIR/bin"
|
||||
CALLS="$TMP_DIR/uci.calls"
|
||||
export CALLS
|
||||
cat >"$TMP_DIR/bin/uci" <<'EOF_UCI'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$*" >>"$CALLS"
|
||||
case "$*" in
|
||||
'-q get safeshield.config.license_key') exit 0 ;;
|
||||
esac
|
||||
exit 0
|
||||
EOF_UCI
|
||||
chmod 755 "$TMP_DIR/bin/uci"
|
||||
PATH="$TMP_DIR/bin:$PATH" sh "$SS_SPEC_ROOT/files/etc/uci-defaults/91_safeshield_remove_legacy_license"
|
||||
ss_spec_assert_file_contains "$CALLS" '-q get safeshield.config.license_key'
|
||||
ss_spec_assert_file_contains "$CALLS" '-q delete safeshield.config.license_key'
|
||||
ss_spec_assert_file_contains "$CALLS" '-q commit safeshield'
|
||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/Makefile" './files/etc/uci-defaults/91_safeshield_remove_legacy_license'
|
||||
)
|
||||
|
||||
ss_case_status_version() (
|
||||
set -eu
|
||||
make_version="$(sed -n 's/^PKG_VERSION:=//p' "$SS_SPEC_ROOT/Makefile")"
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
|
||||
ss_case_device_api_contract() (
|
||||
set -eu
|
||||
BLOCKLIST="$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||
STATISTICS="$SS_SPEC_ROOT/files/usr/lib/safeshield/statistics.sh"
|
||||
|
||||
ss_spec_assert_file_not_contains "$BLOCKLIST" '/api/v1/devices/bootstrap'
|
||||
ss_spec_assert_file_contains "$BLOCKLIST" '/api/v1/devices/sync'
|
||||
ss_spec_assert_file_contains "$STATISTICS" '/api/v1/devices/sync'
|
||||
ss_spec_assert_file_contains "$BLOCKLIST" 'Authorization: ${authorization}'
|
||||
ss_spec_assert_file_contains "$BLOCKLIST" 'Device ${device_credential}'
|
||||
ss_spec_assert_file_not_contains "$BLOCKLIST" "url='https://www.smartsafehub.com/api/v1/licenses/resolve'"
|
||||
ss_spec_assert_file_not_contains "$STATISTICS" "resolve_url='https://www.smartsafehub.com/api/v1/licenses/resolve'"
|
||||
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/config.sh" 'ss_license_key'
|
||||
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/libexec/safeshield-statistics-uploader" 'ss_license_key'
|
||||
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield.uc" 'license_update'
|
||||
ss_spec_assert_file_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/status.uc" 'entitlement: {'
|
||||
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/share/rpcd/ucode/safeshield/status.uc" 'license: {'
|
||||
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh" 'license_plan'
|
||||
ss_spec_assert_file_not_contains "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh" 'license_status'
|
||||
)
|
||||
|
||||
ss_case_device_registration_pending() (
|
||||
set -eu
|
||||
TMP_DIR="$(ss_spec_tmpdir)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT HUP INT TERM
|
||||
SS_TMP_DIR="$TMP_DIR/tmp"
|
||||
SS_API_PAYLOAD="$SS_TMP_DIR/resolve-request.json"
|
||||
SS_API_RESPONSE="$SS_TMP_DIR/resolve-response.json"
|
||||
ss_download_retry=1
|
||||
ss_download_timeout=10
|
||||
mkdir -p "$SS_TMP_DIR"
|
||||
|
||||
ss_should_stop() { return 1; }
|
||||
ss_status_set() { printf '%s=%s\n' "$1" "$2" >>"$TMP_DIR/status"; }
|
||||
ss_status_add_error() { :; }
|
||||
log_info() { :; }
|
||||
log_warn() { :; }
|
||||
log_error() { :; }
|
||||
log_ok() { :; }
|
||||
command_exists() { return 1; }
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
. "$SS_SPEC_ROOT/files/usr/lib/safeshield/blocklist.sh"
|
||||
ss_write_resolve_payload() { printf '{}\n' >"$1"; }
|
||||
ss_device_credential_token() { return 1; }
|
||||
|
||||
if ss_resolve_artifact; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
ss_spec_assert_eq "$SS_RESOLVE_ERROR_CODE" 'device_registration_pending'
|
||||
ss_spec_assert_file_contains "$TMP_DIR/status" 'device_registration_status=pending'
|
||||
ss_spec_assert_file_contains "$TMP_DIR/status" 'health_api_resolve=0'
|
||||
)
|
||||
@@ -529,7 +529,6 @@ EOF_CORE
|
||||
MOCK_REFRESH_RESULT='allowed'
|
||||
MOCK_DENIED_RECHECK_DUE='0'
|
||||
SS_STATISTICS_UPLOAD_TOKEN=''
|
||||
ss_license_key='paid-license-key'
|
||||
ss_should_terminate=0
|
||||
: >"$REFRESH_CALLS"
|
||||
upload_sleep() {
|
||||
@@ -625,14 +624,12 @@ EOF_CORE
|
||||
upload_state_load
|
||||
ss_spec_assert_eq "$upload_state_needs_full" '1'
|
||||
|
||||
# An unlicensed device is blocked locally without asking the Hub for a
|
||||
# statistics credential. The long-lived uploader can remain idle so a later
|
||||
# license update can re-enable it without restarting the collector.
|
||||
MOCK_UPLOAD_ENTITLEMENT='allowed'
|
||||
# Statistics eligibility comes only from device sync. A denied account/plan
|
||||
# is rechecked on the normal entitlement cadence without any local license key.
|
||||
MOCK_UPLOAD_ENTITLEMENT='denied'
|
||||
MOCK_REFRESH_RESULT='allowed'
|
||||
MOCK_DENIED_RECHECK_DUE='1'
|
||||
SS_STATISTICS_UPLOAD_TOKEN='token-old'
|
||||
ss_license_key=''
|
||||
SS_STATISTICS_UPLOAD_TOKEN=''
|
||||
ss_should_terminate=0
|
||||
: >"$REFRESH_CALLS"
|
||||
upload_sleep() {
|
||||
@@ -640,9 +637,9 @@ EOF_CORE
|
||||
return 1
|
||||
}
|
||||
main
|
||||
ss_spec_assert_eq "$MOCK_UPLOAD_ENTITLEMENT" 'denied'
|
||||
ss_spec_assert_eq "$(wc -l <"$REFRESH_CALLS" | tr -d ' ')" '0'
|
||||
ss_spec_assert_eq "$SS_STATISTICS_UPLOAD_TOKEN" ''
|
||||
ss_spec_assert_eq "$MOCK_UPLOAD_ENTITLEMENT" 'allowed'
|
||||
ss_spec_assert_eq "$(wc -l <"$REFRESH_CALLS" | tr -d ' ')" '1'
|
||||
ss_spec_assert_eq "$SS_STATISTICS_UPLOAD_TOKEN" 'token-new'
|
||||
)
|
||||
|
||||
ss_case_statistics_upload_credentials() (
|
||||
|
||||
@@ -64,6 +64,9 @@ ss_case_dead_code_contract() (
|
||||
! grep -Eq '^ss_detect_primary_mac\\(\\)[[:space:]]*\\{' "$BLOCKLIST"
|
||||
! grep -F 'ss_debug=' "$CONFIG"
|
||||
! grep -F '/lib/functions/network.sh' "$INIT"
|
||||
! grep -F 'ss_license_key' "$CONFIG"
|
||||
! grep -F 'license_key:' "$INIT"
|
||||
! grep -Eq '^ss_mask_secret\(\)[[:space:]]*\{' "$UTILS"
|
||||
|
||||
# Keep the replacement/runtime contracts that made the retired helpers obsolete.
|
||||
ss_spec_assert_file_contains "$BLOCKLIST" 'ss_identity_ensure "$model" "$arch"'
|
||||
|
||||
@@ -34,7 +34,7 @@ ss_case_ucode() (
|
||||
cp "$mock_dir"/*.uc "$test_modules/"
|
||||
"$UCODE_BIN" -L "$test_modules" -D "TEST_TMP=\"$test_tmp\"" "$test_file" >/dev/null
|
||||
}
|
||||
for name in core config license refresh rules statistics status runtime; do
|
||||
for name in core config refresh rules statistics status runtime; do
|
||||
run_ucode_test "$name"
|
||||
done
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ let values = {
|
||||
debug: '0',
|
||||
download_retry: '3',
|
||||
statistics_enabled: '1',
|
||||
license_key: 'abcd1234wxyz',
|
||||
device_vendor: 'Junatum',
|
||||
device_model: 'Test Router',
|
||||
device_arch: 'aarch64',
|
||||
@@ -33,12 +32,6 @@ function to_int(v, def) {
|
||||
return type(n) == 'int' ? n : def;
|
||||
}
|
||||
|
||||
function mask_secret(v) {
|
||||
let s = sprintf('%s', v || '');
|
||||
if (!length(s)) return '';
|
||||
if (length(s) <= 8) return '********';
|
||||
return sprintf('%s...%s', substr(s, 0, 4), substr(s, length(s) - 4));
|
||||
}
|
||||
|
||||
function api_error(code, message, field) {
|
||||
let error = { code: code, message: message };
|
||||
@@ -71,7 +64,6 @@ return {
|
||||
cfg: function(name, def) { return values[name] == null ? def : values[name]; },
|
||||
to_bool: to_bool,
|
||||
to_int: to_int,
|
||||
mask_secret: mask_secret,
|
||||
api_error: api_error,
|
||||
uci_commit_option: uci_commit_option,
|
||||
bool_uci: function(v) { return v ? '1' : '0'; }
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let state = {
|
||||
license_key: 'existing-license-key',
|
||||
commits: 0,
|
||||
deletes: 0
|
||||
};
|
||||
|
||||
function trim(s) {
|
||||
return replace(s, /^[\r\n\t ]+|[\r\n\t ]+$/g, '');
|
||||
}
|
||||
|
||||
function mask_secret(v) {
|
||||
let s = sprintf('%s', v || '');
|
||||
if (!length(s)) return '';
|
||||
if (length(s) <= 8) return '********';
|
||||
return sprintf('%s...%s', substr(s, 0, 4), substr(s, length(s) - 4));
|
||||
}
|
||||
|
||||
function api_error(code, message, field) {
|
||||
let error = { code: code, message: message };
|
||||
if (field) error.field = field;
|
||||
return { ok: false, error: error };
|
||||
}
|
||||
|
||||
return {
|
||||
state: state,
|
||||
reload_uci: function() { return true; },
|
||||
cfg: function(name, def) { return name == 'license_key' ? state.license_key : def; },
|
||||
trim: trim,
|
||||
mask_secret: mask_secret,
|
||||
api_error: api_error,
|
||||
uci_commit_option: function(name, value) {
|
||||
state.license_key = value;
|
||||
state.commits++;
|
||||
return true;
|
||||
},
|
||||
uci_delete_option: function(name) {
|
||||
state.license_key = '';
|
||||
state.deletes++;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let state = { refresh_count: 0, statistics_reset_count: 0, accepted: true, reason: '' };
|
||||
|
||||
return {
|
||||
state: state,
|
||||
reset_statistics_upload_state: function() {
|
||||
state.statistics_reset_count++;
|
||||
},
|
||||
start_refresh_async: function() {
|
||||
state.refresh_count++;
|
||||
return { accepted: state.accepted, reason: state.reason };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
let ubus = require('ubus');
|
||||
let state = { calls: [], timeout: false };
|
||||
|
||||
function popen(command, mode) {
|
||||
push(state.calls, command);
|
||||
if (state.timeout) return null;
|
||||
let name = match(command, /\"name\":\"([^\"]+)\"/);
|
||||
let service_name = name ? name[1] : null;
|
||||
let instance = service_name ? ubus.state.services[service_name] : null;
|
||||
let response = {};
|
||||
if (instance != null) response[service_name] = instance;
|
||||
return {
|
||||
read: function() { return sprintf('%J', response); },
|
||||
close: function() { return true; }
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
state: state,
|
||||
popen: popen,
|
||||
unlink: function() { return true; }
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
return {
|
||||
service_status: function() {
|
||||
return { state: 'running', running: true, lookup_ok: true, instances: { statistics: { running: true } } };
|
||||
},
|
||||
service_instance_running: function(name, instance) {
|
||||
return name == 'safeshield' && instance == 'statistics';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ let config = {
|
||||
require_wan: '1',
|
||||
refresh_interval_s: '28800',
|
||||
boot_start_delay_s: '30',
|
||||
license_key: 'abcd1234wxyz',
|
||||
apply_local_overrides: '1',
|
||||
device_vendor: 'Junatum',
|
||||
device_model: 'Test Router',
|
||||
@@ -41,8 +40,8 @@ let state = {
|
||||
health_dns_runtime: '1',
|
||||
blocklist_installed: '1',
|
||||
blocklist_file_size_kb: '2048',
|
||||
license_plan: 'pro',
|
||||
license_status: 'active'
|
||||
entitlement_plan: 'pro',
|
||||
entitlement_status: 'active'
|
||||
},
|
||||
warnings: [],
|
||||
errors: []
|
||||
@@ -67,12 +66,6 @@ function to_int(v, def) {
|
||||
return type(n) == 'int' ? n : def;
|
||||
}
|
||||
|
||||
function mask_secret(v) {
|
||||
let s = sprintf('%s', v || '');
|
||||
if (!length(s)) return '';
|
||||
if (length(s) <= 8) return '********';
|
||||
return sprintf('%s...%s', substr(s, 0, 4), substr(s, length(s) - 4));
|
||||
}
|
||||
|
||||
return {
|
||||
PKG_NAME: 'safeshield',
|
||||
@@ -93,6 +86,5 @@ return {
|
||||
file_size_kb: function() { return 0; },
|
||||
to_bool: to_bool,
|
||||
to_optional_bool: to_optional_bool,
|
||||
to_int: to_int,
|
||||
mask_secret: mask_secret
|
||||
to_int: to_int
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
let state = { service_running: true, dnsmasq_running: true };
|
||||
|
||||
let state = { service_running: true, dnsmasq_running: true, lookup_ok: true };
|
||||
return {
|
||||
state: state,
|
||||
service_status: function(name) {
|
||||
let running = name == 'dnsmasq' ? state.dnsmasq_running : state.service_running;
|
||||
return { running: running && state.lookup_ok, state: !state.lookup_ok ? 'unknown' : (running ? 'running' : 'stopped'), lookup_ok: state.lookup_ok };
|
||||
},
|
||||
service_running: function() { return state.service_running; },
|
||||
dnsmasq_running: function() { return state.dnsmasq_running; }
|
||||
};
|
||||
@@ -8,8 +8,6 @@ let built = config.build();
|
||||
assert(built.schema.name == 'safeshield.config', 'config schema name is preserved');
|
||||
assert(built.values.enabled == false, 'disabled state is normalized to boolean');
|
||||
assert(built.values.download_retry == 3, 'integer options are normalized');
|
||||
assert(built.license.configured == true, 'configured license is reported');
|
||||
assert(built.license.key_masked == 'abcd...wxyz', 'license key is masked');
|
||||
assert(built.device.memory_mb == 256, 'device memory is normalized to integer');
|
||||
|
||||
let invalid_values = config.update({ args: { values: 'not-an-object' } });
|
||||
@@ -27,8 +25,8 @@ assert(unknown.ok == false && unknown.error.code == 'unknown_option', 'unknown o
|
||||
let dedicated = config.update({ args: { values: { enabled: true } } });
|
||||
assert(dedicated.ok == false && dedicated.error.code == 'dedicated_method_required', 'enabled requires dedicated method');
|
||||
|
||||
let dedicated_license = config.update({ args: { values: { license_key: 'new-key' } } });
|
||||
assert(dedicated_license.ok == false && dedicated_license.error.code == 'dedicated_method_required', 'license_key requires dedicated method');
|
||||
let retired_license_key = config.update({ args: { values: { license_key: 'new-key' } } });
|
||||
assert(retired_license_key.ok == false && retired_license_key.error.code == 'unknown_option', 'retired license_key is not a writable option');
|
||||
|
||||
runtime.state.last_action = '';
|
||||
let statistics = config.update({ args: { values: { statistics_enabled: false } } });
|
||||
|
||||
@@ -22,9 +22,6 @@ assert(core.to_int('42', 7) == 42, 'to_int converts numeric strings');
|
||||
assert(core.to_int('', 7) == 7, 'to_int uses default for empty values');
|
||||
assert(core.to_int(null, 7) == 7, 'to_int uses default for null');
|
||||
|
||||
assert(core.mask_secret('') == '', 'mask_secret keeps empty secrets empty');
|
||||
assert(core.mask_secret('short') == '********', 'mask_secret fully masks short secrets');
|
||||
assert(core.mask_secret('abcd1234wxyz') == 'abcd...wxyz', 'mask_secret partially masks long secrets');
|
||||
|
||||
let error = core.api_error('invalid_value', 'Invalid value', 'field_name');
|
||||
assert(error.ok == false, 'api_error marks response as failed');
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
let license = require('license');
|
||||
|
||||
let current = license.get();
|
||||
assert(current.ok == true && current.license.key == 'existing-license-key', 'license_get returns current key');
|
||||
|
||||
let missing = license.update({ args: {} });
|
||||
assert(missing.ok == false && missing.error.code == 'missing_argument', 'license_update requires license_key');
|
||||
|
||||
let multiline = license.update({ args: { license_key: 'line1\nline2' } });
|
||||
assert(multiline.ok == false && multiline.error.code == 'invalid_license_key', 'license_update rejects line breaks');
|
||||
|
||||
let oversized_key = '';
|
||||
for (let i = 0; i < 513; i++) oversized_key += 'x';
|
||||
let too_long = license.update({ args: { license_key: oversized_key } });
|
||||
assert(too_long.ok == false && too_long.error.code == 'invalid_license_key', 'license_update rejects keys longer than 512 characters');
|
||||
|
||||
runtime.state.refresh_count = 0;
|
||||
runtime.state.statistics_reset_count = 0;
|
||||
let updated = license.update({ args: { license_key: ' new-license-key ' } });
|
||||
assert(updated.ok == true && updated.changed == true, 'license_update accepts a new key');
|
||||
assert(core.state.license_key == 'new-license-key', 'license_update trims key before saving');
|
||||
assert(updated.license.key_masked == 'new-...-key', 'license_update masks saved key');
|
||||
assert(runtime.state.refresh_count == 1, 'license_update requests refresh after change');
|
||||
assert(runtime.state.statistics_reset_count == 1, 'license_update resets statistics upload entitlement after change');
|
||||
|
||||
runtime.state.refresh_count = 0;
|
||||
runtime.state.statistics_reset_count = 0;
|
||||
let unchanged = license.update({ args: { license_key: 'new-license-key' } });
|
||||
assert(unchanged.ok == true && unchanged.changed == false, 'unchanged license does not write');
|
||||
assert(runtime.state.refresh_count == 0, 'unchanged license does not refresh');
|
||||
assert(runtime.state.statistics_reset_count == 0, 'unchanged license keeps statistics upload entitlement');
|
||||
|
||||
runtime.state.statistics_reset_count = 0;
|
||||
let cleared = license.update({ args: { license_key: '' } });
|
||||
assert(cleared.ok == true && cleared.changed == true, 'empty license clears configured key');
|
||||
assert(core.state.license_key == '' && core.state.deletes == 1, 'empty license uses delete helper');
|
||||
assert(cleared.license.configured == false, 'cleared license is reported unconfigured');
|
||||
assert(runtime.state.statistics_reset_count == 1, 'clearing the license resets statistics upload entitlement');
|
||||
|
||||
print('ucode license tests: ok\n');
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
let core = require('core');
|
||||
let ubus = require('ubus');
|
||||
let fs = require('fs');
|
||||
let runtime = require('runtime');
|
||||
|
||||
ubus.state.services.safeshield = {
|
||||
@@ -10,6 +11,7 @@ ubus.state.services.safeshield = {
|
||||
stopped: { running: false }
|
||||
}
|
||||
};
|
||||
assert(runtime.service_status('safeshield', false).state == 'running', 'runtime distinguishes running services');
|
||||
assert(runtime.service_running('safeshield') == true, 'service_running accepts any running instance');
|
||||
assert(runtime.service_instance_running('safeshield', 'refreshd') == true, 'service_instance_running finds a running instance');
|
||||
assert(runtime.service_instance_running('safeshield', 'stopped') == false, 'service_instance_running preserves stopped state');
|
||||
@@ -20,6 +22,18 @@ ubus.state.services.dnsmasq = { instances: { main: { running: true } } };
|
||||
assert(runtime.dnsmasq_running() == true, 'dnsmasq_running accepts a running dnsmasq instance');
|
||||
ubus.state.services.dnsmasq.instances.main.running = false;
|
||||
assert(runtime.dnsmasq_running() == false, 'dnsmasq_running rejects stopped dnsmasq instances');
|
||||
assert(length(fs.state.calls) == 6, 'supported service checks use a separate bounded ubus client while unsupported names are rejected locally');
|
||||
for (let call in fs.state.calls) {
|
||||
assert(index(call, 'ubus -t 2 call service list') >= 0, 'service lookup uses ubus built-in timeout without an external timeout binary');
|
||||
}
|
||||
fs.state.timeout = true;
|
||||
assert(runtime.service_running('safeshield') == false, 'failed or timed out service lookup fails closed');
|
||||
assert(runtime.service_status('safeshield', true).state == 'unknown', 'lookup failure is exposed as unknown');
|
||||
fs.state.timeout = false;
|
||||
let calls_before = length(fs.state.calls);
|
||||
assert(runtime.service_status('safeshield', true).lookup_ok == true, 'failed lookups are not cached');
|
||||
assert(runtime.service_status('safeshield', true).lookup_ok == true, 'successful lookup can be reused briefly');
|
||||
assert(length(fs.state.calls) == calls_before + 1, 'repeated successful lookups share the cached result');
|
||||
|
||||
let action = runtime.run_service_action('status', 1000);
|
||||
assert(action.ok == true && action.rc == 0, 'run_service_action reports successful service commands');
|
||||
|
||||
@@ -5,6 +5,7 @@ let statistics = require('statistics');
|
||||
let result = statistics.build();
|
||||
assert(result.schema.name == 'safeshield.statistics' && result.schema.version == 3, 'statistics schema is preserved');
|
||||
assert(result.enabled == true && result.effective_enabled == true && result.available == true, 'configured and effective statistics availability are reported');
|
||||
assert(result.collector_state == 'running' && result.collector_lookup_ok == true, 'collector lookup detail is exposed');
|
||||
assert(result.collector_running == true, 'statistics collector state is reported');
|
||||
assert(result.snapshot_interval_s == 30 && result.effective_snapshot_interval_s == 30 && result.retention_hours == 24, 'statistics config values are normalized');
|
||||
assert(result.generation_id == 'generation-test', 'statistics generation is preserved');
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
let core = require('core');
|
||||
let runtime = require('runtime');
|
||||
let status = require('status');
|
||||
|
||||
let ready = status.build();
|
||||
assert(ready.runtime.refreshd_state == 'running' && ready.runtime.dnsmasq_state == 'running' && ready.runtime.refreshd_lookup_ok == true, 'healthy status includes successful procd lookup details');
|
||||
runtime.state.lookup_ok = false;
|
||||
let unknown = status.build();
|
||||
assert(unknown.runtime.refreshd_state == 'unknown' && unknown.runtime.refreshd_lookup_ok == false && unknown.active == false, 'unknown service lookups do not claim running');
|
||||
runtime.state.lookup_ok = true;
|
||||
assert(ready.status == 'ready', 'status preserves ready state');
|
||||
assert(ready.summary.label == 'Ready' && ready.summary.severity == 'info', 'ready summary is generated');
|
||||
assert(index(ready.summary.message, '42000 rules active') >= 0, 'ready summary includes active rule count');
|
||||
@@ -11,7 +17,8 @@ assert(ready.artifact.source_count == 2, 'status reports artifact source count')
|
||||
assert(ready.artifact.block_source_count == 1 && ready.artifact.allow_source_count == 1, 'status reports artifact action counts');
|
||||
assert(ready.sources.items[0].last_result == 'ok', 'healthy API source is reported ok');
|
||||
assert(ready.blocklist.installed == true && ready.blocklist.file_size_kb == 2048, 'blocklist state is normalized');
|
||||
assert(ready.license.configured == true && ready.license.key_masked == 'abcd...wxyz', 'license state is masked');
|
||||
assert(ready.entitlement.plan == 'pro' && ready.entitlement.status == 'active', 'Hub entitlement is exposed under the entitlement field');
|
||||
assert(ready.license == null, 'status does not expose the retired license status field');
|
||||
assert(ready.device.device_code == 'xiaomi-ax3000t', 'status exposes the canonical device code');
|
||||
assert(ready.device.device_code_source == 'smartsafehub_firmware', 'status exposes the device code source');
|
||||
assert(ready.health.overall == 'ok', 'healthy ready state reports overall ok');
|
||||
|
||||
Reference in new issue
Block a user