diff --git a/CHECKLIST.md b/CHECKLIST.md index ddf4ac5..6a7fad6 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md @@ -87,6 +87,10 @@ - [x] `route_show`; - [x] `interfaces_show`; - [x] `pkg_list_installed` / `opkg_list_installed`; + - [x] `pkg_update` / `opkg_update`; + - [x] `pkg_list_upgradable` / `opkg_list_upgradable`; + - [x] `pkg_install` / `opkg_install`; + - [x] `pkg_remove` / `opkg_remove`; - [ ] OpenWrt cross-build/package integration. ## Этап 2: MVP Server API diff --git a/agent/README.md b/agent/README.md index d0c9d22..669a1c5 100644 --- a/agent/README.md +++ b/agent/README.md @@ -57,6 +57,10 @@ The Go agent is not the production command runner yet. It currently supports the - `route_show` - `interfaces_show` - `pkg_list_installed` / `opkg_list_installed` +- `pkg_update` / `opkg_update` +- `pkg_list_upgradable` / `opkg_list_upgradable` +- `pkg_install` / `opkg_install` +- `pkg_remove` / `opkg_remove` Other queued commands are reported as failed with a clear message until the shell allowlist is migrated command by command. diff --git a/agent/go/cmd/rmm-agent/main.go b/agent/go/cmd/rmm-agent/main.go index 9eed6b8..faea108 100644 --- a/agent/go/cmd/rmm-agent/main.go +++ b/agent/go/cmd/rmm-agent/main.go @@ -317,6 +317,22 @@ func runCommand(ctx context.Context, cmd command) (string, int) { return execCommand(ctx, 10*time.Second, "ip", "-o", "addr", "show") case "pkg_list_installed", "opkg_list_installed": return runPackageCommand(ctx, "list_installed") + case "pkg_update", "opkg_update": + return runPackageCommand(ctx, "update") + case "pkg_list_upgradable", "opkg_list_upgradable": + return runPackageCommand(ctx, "list_upgradable") + case "pkg_install", "opkg_install": + packageName := strings.TrimSpace(args["package"]) + if !safePackageName(packageName) { + return "package name is invalid\n", 2 + } + return runPackageCommand(ctx, "install", packageName) + case "pkg_remove", "opkg_remove": + packageName := strings.TrimSpace(args["package"]) + if !safePackageName(packageName) { + return "package name is invalid\n", 2 + } + return runPackageCommand(ctx, "remove", packageName) default: return fmt.Sprintf("go agent preview does not implement command %q yet; shell agent remains the production command runner\n", cmd.Type), 2 } @@ -348,13 +364,51 @@ func safeHostName(value string) bool { return true } -func runPackageCommand(ctx context.Context, action string) (string, int) { +func safePackageName(value string) bool { + if value == "" || len(value) > 128 { + return false + } + for _, r := range value { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { + continue + } + switch r { + case '_', '.', '+', '-': + continue + default: + return false + } + } + return true +} + +func runPackageCommand(ctx context.Context, action string, packageName ...string) (string, int) { pm := packageManager() + pkg := "" + if len(packageName) > 0 { + pkg = packageName[0] + } switch pm + ":" + action { case "apk:list_installed": return execCommand(ctx, 30*time.Second, "apk", "list", "-I") + case "apk:update": + return execCommand(ctx, 60*time.Second, "apk", "update") + case "apk:list_upgradable": + return execCommand(ctx, 30*time.Second, "apk", "list", "--upgradeable") + case "apk:install": + return execCommand(ctx, 120*time.Second, "apk", "add", pkg) + case "apk:remove": + return execCommand(ctx, 120*time.Second, "apk", "del", pkg) case "opkg:list_installed": return execCommand(ctx, 30*time.Second, "opkg", "list-installed") + case "opkg:update": + return execCommand(ctx, 60*time.Second, "opkg", "update") + case "opkg:list_upgradable": + return execCommand(ctx, 30*time.Second, "opkg", "list-upgradable") + case "opkg:install": + return execCommand(ctx, 120*time.Second, "opkg", "install", pkg) + case "opkg:remove": + return execCommand(ctx, 120*time.Second, "opkg", "remove", pkg) default: return "no supported package manager found\n", 2 }