mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-14 20:48:17 +08:00
163 lines
6.0 KiB
Go
163 lines
6.0 KiB
Go
package control
|
||
|
||
import (
|
||
"fmt"
|
||
"slices"
|
||
"strings"
|
||
)
|
||
|
||
// Kind separates calls that only read from calls that change something.
|
||
//
|
||
// It is not decoration. Read calls are what the LuCI page polls every few
|
||
// seconds, so they must answer from cached state: a "read" that authenticated
|
||
// or probed the network would turn an idle browser tab into traffic on the
|
||
// campus gateway.
|
||
type Kind string
|
||
|
||
const (
|
||
// KindRead answers from state already held. No network, no writes.
|
||
KindRead Kind = "read"
|
||
// KindMutate changes configuration, or queues or cancels work.
|
||
KindMutate Kind = "mutate"
|
||
// KindTask starts bounded background work and returns a handle. The work
|
||
// outliving the request is the point: a browser that navigates away must
|
||
// not cancel an install.
|
||
KindTask Kind = "task"
|
||
)
|
||
|
||
// Method describes one callable name.
|
||
type Method struct {
|
||
Name string
|
||
Kind Kind
|
||
// Note records the constraint that is easy to violate later, in the place
|
||
// someone adding a handler will actually read.
|
||
Note string
|
||
}
|
||
|
||
// catalogue is the complete set of method names this protocol will ever accept.
|
||
//
|
||
// Fixed, not assembled at runtime: a registry that accepted whatever a handler
|
||
// registered would make the wire surface depend on initialisation order, and
|
||
// "which methods exist" is a security property here, not an implementation
|
||
// detail.
|
||
var catalogue = []Method{
|
||
{"status.get", KindRead, "一次组合快照;不得触发认证或同步探测"},
|
||
{"schema.get", KindRead, "只读字段契约,供 LuCI 与 CLI 共用一份默认值"},
|
||
{"capabilities.get", KindRead, "本设备实际具备的能力,缺工具报 UnsupportedCapability"},
|
||
{"version.get", KindRead, ""},
|
||
|
||
{"config.get", KindRead, "不返回秘密明文"},
|
||
{"config.export", KindRead, "明确 include_secrets 才导出凭据;不得用于状态轮询"},
|
||
{"config.import", KindMutate, "严格校验备份;预览不写盘;提交需要 expected_revision"},
|
||
{"config.validate", KindRead, "纯校验,不写盘"},
|
||
{"config.apply", KindMutate, "必须带 expected_revision;冲突时保留旧配置"},
|
||
|
||
{"campus.get", KindRead, "只有明确授权的详情响应才包含密码"},
|
||
{"campus.upsert", KindMutate, "presence 明确:字段省略=保持,字符串=替换"},
|
||
{"campus.remove", KindMutate, "删除后必须修复 active/default 指针"},
|
||
{"campus.set_default", KindMutate, "同时更新同类 active/default 指针"},
|
||
|
||
{"hotspot.get", KindRead, ""},
|
||
{"hotspot.upsert", KindMutate, ""},
|
||
{"hotspot.remove", KindMutate, ""},
|
||
{"hotspot.set_default", KindMutate, ""},
|
||
|
||
{"action.submit", KindMutate, "queued 不等于 succeeded;重复 key 返回同一任务"},
|
||
{"action.get", KindRead, ""},
|
||
{"action.cancel", KindMutate, "取消不保证网关没收到请求"},
|
||
|
||
{"presets.list", KindRead, ""},
|
||
{"presets.refresh", KindTask, "远程刷新任务化,不得阻塞状态轮询"},
|
||
{"user_presets.get", KindRead, ""},
|
||
{"user_presets.set", KindMutate, "独立 revision,CAS 只作用于用户预设文件"},
|
||
|
||
{"detect.environment", KindTask, "未选线路时不开始探测"},
|
||
{"detect.acid", KindTask, "完整输入路径先探测再归一化 origin"},
|
||
{"detect.operators", KindTask, "被动获取页面选项;selected 不等于本人运营商"},
|
||
{"detect.identity", KindTask, "只读身份,绝不携带输入的密码"},
|
||
{"detect.verify", KindTask, "唯一可主动认证的探测;候选上限 5"},
|
||
|
||
{"setup_wifi.start", KindTask, "job 与当前会话关联"},
|
||
{"setup_wifi.status", KindRead, ""},
|
||
{"setup_wifi.cancel", KindMutate, "幂等"},
|
||
{"setup_wifi.commit", KindMutate, "幂等"},
|
||
{"setup_wifi.account", KindMutate, "用向导结果保存账号"},
|
||
|
||
{"log.tail", KindRead, "增量游标;channel=plugin/network"},
|
||
{"log.download", KindRead, "有界读取"},
|
||
{"log.clear", KindMutate, "只清本项目日志,不动系统全局日志"},
|
||
|
||
{"update.check", KindTask, ""},
|
||
{"update.start", KindTask, "只接受已验证的 release/plan ID,绝不接受任意 URL 或 shell"},
|
||
{"update.status", KindRead, "主服务停止时也要能从固定路径只读续查"},
|
||
|
||
{"schools.list", KindRead, ""},
|
||
{"schools.inspect", KindRead, ""},
|
||
{"school.command", KindMutate, "白名单命令,不能遮蔽核心命令"},
|
||
}
|
||
|
||
var catalogueByName = func() map[string]Method {
|
||
out := make(map[string]Method, len(catalogue))
|
||
for _, method := range catalogue {
|
||
out[method.Name] = method
|
||
}
|
||
return out
|
||
}()
|
||
|
||
// Catalogue returns every declared method, sorted by name.
|
||
func Catalogue() []Method {
|
||
out := slices.Clone(catalogue)
|
||
slices.SortFunc(out, func(a, b Method) int { return strings.Compare(a.Name, b.Name) })
|
||
return out
|
||
}
|
||
|
||
// Lookup reports the declared method with this name.
|
||
func Lookup(name string) (Method, bool) {
|
||
method, ok := catalogueByName[name]
|
||
return method, ok
|
||
}
|
||
|
||
// MethodNames lists every declared name.
|
||
func MethodNames() []string {
|
||
out := make([]string, 0, len(catalogue))
|
||
for _, method := range catalogue {
|
||
out = append(out, method.Name)
|
||
}
|
||
slices.Sort(out)
|
||
return out
|
||
}
|
||
|
||
// validateCatalogue reports the first malformed or duplicated entry.
|
||
//
|
||
// It returns an error rather than panicking so the rules themselves can be
|
||
// tested; init turns a failure into a panic, because a protocol surface that is
|
||
// wrong at startup must not go on to serve requests.
|
||
func validateCatalogue(methods []Method) error {
|
||
seen := make(map[string]struct{}, len(methods))
|
||
for _, method := range methods {
|
||
family, action, found := strings.Cut(method.Name, ".")
|
||
if !found || family == "" || action == "" {
|
||
return fmt.Errorf("control: method %q is not family.action", method.Name)
|
||
}
|
||
switch method.Kind {
|
||
case KindRead, KindMutate, KindTask:
|
||
default:
|
||
return fmt.Errorf("control: method %q has unknown kind %q",
|
||
method.Name, method.Kind)
|
||
}
|
||
if _, duplicate := seen[method.Name]; duplicate {
|
||
return fmt.Errorf("control: duplicate method in catalogue: %s", method.Name)
|
||
}
|
||
seen[method.Name] = struct{}{}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// The check runs at init so a malformed entry fails the build's first test
|
||
// rather than the first request.
|
||
func init() {
|
||
if err := validateCatalogue(catalogue); err != nil {
|
||
panic(err.Error())
|
||
}
|
||
}
|