Files

163 lines
6.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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())
}
}