From ba1e9c2741716b2b02cf263f705e8f261fa6f3b2 Mon Sep 17 00:00:00 2001 From: pg9182 <96569817+pg9182@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:07:38 -0400 Subject: [PATCH 1/3] pkg/nsrule: Implement basic ruleset parsing and evaluation --- go.mod | 1 + go.sum | 10 ++ pkg/nsrule/env.go | 89 ++++++++++++++++++ pkg/nsrule/rule.go | 222 +++++++++++++++++++++++++++++++++++++++++++++ pkg/nsrule/tags.go | 185 +++++++++++++++++++++++++++++++++++++ 5 files changed, 507 insertions(+) create mode 100644 pkg/nsrule/env.go create mode 100644 pkg/nsrule/rule.go create mode 100644 pkg/nsrule/tags.go diff --git a/go.mod b/go.mod index db485e6..d084857 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.0 require ( github.com/VictoriaMetrics/metrics v1.23.1 github.com/andybalholm/cascadia v1.3.1 + github.com/antonmedv/expr v1.15.2 github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682 github.com/hashicorp/go-envparse v0.1.0 github.com/jmoiron/sqlx v1.3.5 diff --git a/go.sum b/go.sum index 8fc9c42..09a5686 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,13 @@ github.com/VictoriaMetrics/metrics v1.23.1 h1:/j8DzeJBxSpL2qSIdqnRFLvQQhbJyJbbEi github.com/VictoriaMetrics/metrics v1.23.1/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/antonmedv/expr v1.15.2 h1:afFXpDWIC2n3bF+kTZE1JvFo+c34uaM3sTqh8z0xfdU= +github.com/antonmedv/expr v1.15.2/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682 h1:Ce5LRUcDnICPpYjWych45AXKaV61l9oqqfMd1hORNPg= github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682/go.mod h1:cDq9S+BVx7XyKnnivCLcKW1oUTnXHkUSs6+LFs4ZrXA= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -32,12 +36,16 @@ github.com/mmcloughlin/geohash v0.10.0/go.mod h1:oNZxQo5yWJh0eMQEP/8hwQuVx9Z9tjw github.com/pg9182/ip2x v1.0.0 h1:aNIWIjzFYmaVHIsbT6OYDVgiySQkgjmecySvjXOCgsU= github.com/pg9182/ip2x v1.0.0/go.mod h1:iJzts7yWZDWUaldqNGFVOZ0MW9uYrGk1hv9R/wTJdNQ= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= @@ -57,3 +65,5 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/nsrule/env.go b/pkg/nsrule/env.go new file mode 100644 index 0000000..94b0acf --- /dev/null +++ b/pkg/nsrule/env.go @@ -0,0 +1,89 @@ +package nsrule + +import ( + "maps" + "reflect" + + "github.com/antonmedv/expr" +) + +// Env contains data used to evaluate rules. +type Env map[string]any + +var ( + dummyEnv = Env{} + defaultEnv = Env{} + extraOptions []expr.Option +) + +// NewEnv shallow-copies the default values into a new Env. +func NewEnv() Env { + return maps.Clone(defaultEnv) +} + +func define[T any](name string, def T, optional bool) func(Env, T) { + if name == "" { + panic("define: name is required") + } + if _, ok := dummyEnv[name]; ok { + panic("define: name is already used") + } + dummyEnv[name] = def + if !optional { + defaultEnv[name] = def + } + return func(e Env, v T) { e[name] = v } +} + +// Define registers a variable with the provided name defaulting to the zero +// value. For parse-time expression type-checking to work, T should not be any. +func Define[T any](name string) func(Env, T) { + var zero T + return define[T](name, zero, false) +} + +// DefineOptional registers an optional variable with the provided name. For parse-time +// expression type-checking to work, T should not be any. +func DefineOptional[T any](name string) func(Env, T) { + var zero T + return define[T](name, zero, true) +} + +// DefineDefault registers a variable with the provided name. For parse-time +// expression type-checking to work, T should not be any. +func DefineDefault[T any](name string, def T) func(Env, T) { + return define[T](name, def, false) +} + +// DefineOperator overloads an operator. +func DefineOperator[T any](op string, fn T) string { + name := op + " " + reflect.TypeOf(fn).String() + extraOptions = append(extraOptions, expr.Operator(op, name)) + define[T](name, fn, false) + return name +} + +// DefineOperatorCompare is shorthand for using DefineOperator to override +// comparison operators. +func DefineOperatorCompare[A, B any](cmp func(a A, b B) int) []string { + var ops []string + ops = append(ops, DefineOperator("<", func(a A, b B) bool { + return cmp(a, b) < 0 + })) + ops = append(ops, DefineOperator("<=", func(a A, b B) bool { + return cmp(a, b) <= 0 + })) + ops = append(ops, DefineOperator("==", func(a A, b B) bool { + return cmp(a, b) == 0 + })) + ops = append(ops, DefineOperator("!=", func(a A, b B) bool { + return cmp(a, b) != 0 + })) + ops = append(ops, DefineOperator(">=", func(a A, b B) bool { + return cmp(a, b) >= 0 + })) + ops = append(ops, DefineOperator(">", func(a A, b B) bool { + return cmp(a, b) > 0 + })) + return ops +} diff --git a/pkg/nsrule/rule.go b/pkg/nsrule/rule.go new file mode 100644 index 0000000..baa6af0 --- /dev/null +++ b/pkg/nsrule/rule.go @@ -0,0 +1,222 @@ +// Package nsrule provides a mechanism for adding arbitrary tags to requests. +package nsrule + +import ( + "bufio" + "fmt" + "io" + "io/fs" + "path" + "strings" + "sync/atomic" + "unicode" + + "github.com/antonmedv/expr" + "github.com/antonmedv/expr/vm" +) + +// RuleSet is a goroutine-safe container holding rules from a directory. +type RuleSet struct { + rules atomic.Pointer[[]Rule] +} + +// LoadFS loads rules from the provided filesystem in lexical order, replacing +// all existing ones. On error, the ruleset is left as-is. +func (s *RuleSet) LoadFS(fsys fs.FS) error { + var rules []Rule + if err := fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + f, err := fsys.Open(p) + if err != nil { + return err + } + defer f.Close() + + r, err := ParseRules(f, path.Clean(p)) + if err != nil { + return fmt.Errorf("parse rules from %q: %w", p, err) + } + rules = append(rules, r...) + } + return nil + }); err != nil { + return err + } + s.rules.Store(&rules) + return nil +} + +// Evaluate evaluates r into t (which should not be nil) against e. The returned +// error list will almost always be nil since expressions are checked during +// parsing. +func (s *RuleSet) Evaluate(e Env, t Tags) []error { + var errs []error + if rs := s.rules.Load(); rs != nil { + for _, r := range *rs { + if err := r.Evaluate(e, t); err != nil { + errs = append(errs, err) + } + } + } + return errs +} + +// Rule is a single rule consisting of an expression and tag mutations. +type Rule struct { + name string + line int + expr *vm.Program + muts []tagMut +} + +// ParseRules parses rules from r, labeling them with name if provided. +// +// Each rule consists of an expression, continued on indented lines, followed by +// one or more further indented lines specifying tag mutations, like: +// +// expression +// continued expression +// continued expression +// tag mutation +// tag mutation +// +// The exact amount and type of indentation doesn't matter, but has to be +// consistent within a rule. Blank lines or lines starting with # ignoring +// preceding whitespace are ignored. +// +// Expressions are checked for syntax errors and undefined names, but tag +// mutations are only checked for syntax errors. +func ParseRules(r io.Reader, name string) ([]Rule, error) { + var ( + rs []Rule + sc = bufio.NewScanner(r) + + line string + lineN int + expB strings.Builder + expN int + muts []string + mutNs []int + last int // last indentation + level int + ) + for eof := false; !eof; { + expLines: + for { + if !sc.Scan() { + eof = true + break expLines + } else { + line = sc.Text() + lineN++ + } + + // ignore blank lines and comments + if x := strings.TrimSpace(line); x == "" || strings.HasPrefix(x, "#") { + continue + } + + // determine indentation + var indent int + for _, x := range line { + if !unicode.IsSpace(x) { + break + } + indent++ + } + + // parse + if indent == 0 { + break expLines + } + if expB.Len() == 0 { + return rs, fmt.Errorf("line %d: expected rule expression start, got indented line", lineN) + } + if indent > last { + if level++; level > 2 { + return rs, fmt.Errorf("line %d: too many indentation levels", lineN) + } + // we have another indent level, so tack the mutation lines onto + // the expression + for _, x := range muts { + expB.WriteByte('\n') + expB.WriteString(x) + } + muts = muts[:0] + mutNs = mutNs[:0] + last = indent + } + if indent != last { + return rs, fmt.Errorf("line %d: unexpected de-indentation", lineN) + } + // we have another line at the current indent level, so assume + // it's a mutation + muts = append(muts, line) + mutNs = append(mutNs, lineN) + } + + // process the pending rule + if expB.Len() != 0 { + + // ensure the rule is complete + if len(muts) == 0 { + return rs, fmt.Errorf("line %d: expected rule (expression %q) to contain tag mutations", lineN, expB.String()) + } + + // compile the rule + r := Rule{ + name: name, + line: expN, + } + if v, err := expr.Compile(expB.String(), append([]expr.Option{expr.AsBool(), expr.Optimize(true), expr.Env(dummyEnv)}, extraOptions...)...); err != nil { // TODO: dummy env + return rs, fmt.Errorf("line %d: compile rule expression: %w", expN, err) + } else { + r.expr = v + } + r.muts = make([]tagMut, len(muts)) + for i := range r.muts { + if v, err := parseTagMut(muts[i]); err != nil { + return rs, fmt.Errorf("line %d: parse tag mutation: %w", mutNs[i], err) + } else { + r.muts[i] = v + } + } + rs = append(rs, r) + + // clear the rule state + expB.Reset() + expN = 0 + muts = muts[:0] + mutNs = mutNs[:0] + last = 0 + level = 0 + } + + // start the new rule + if !eof { + expB.WriteString(line) + expN = lineN + } + } + return rs, sc.Err() +} + +// Evaluate evaluates r into t (which should not be nil) against e. The returned +// error will almost always be nil since expressions are checked during parsing. +func (r Rule) Evaluate(e Env, t Tags) error { + v, err := expr.Run(r.expr, e) + if err != nil { + return fmt.Errorf("evaluate rule at %s:%d: %w", r.name, r.line, err) + } + if v.(bool) { + if t != nil { + for _, m := range r.muts { + m.Apply(t) + } + } + } + return nil +} diff --git a/pkg/nsrule/tags.go b/pkg/nsrule/tags.go new file mode 100644 index 0000000..3f4cc16 --- /dev/null +++ b/pkg/nsrule/tags.go @@ -0,0 +1,185 @@ +package nsrule + +import ( + "fmt" + "slices" + "sort" + "strconv" + "strings" +) + +// Tags contains case-sensitive attributes. If nil, all methods will return the +// zero value. +type Tags map[string][]string + +// String returns a human-readable representation of t. +func (t Tags) String() string { + var b []byte + if t != nil { + for i, k := range t.Names() { + if i != 0 { + b = append(b, '\n') + } + b = append(b, k...) + for i, v := range t[k] { + if i == 0 { + b = append(b, " = "...) + } else { + b = append(b, ", "...) + } + b = strconv.AppendQuote(b, v) + } + } + } + return string(b) +} + +// Names gets the names of all set tags in lexical order. +func (t Tags) Names() []string { + var ns []string + if t != nil { + for k, v := range t { + if len(v) != 0 { + ns = append(ns, k) + } + } + sort.Strings(ns) + } + return ns +} + +// Get gets the first value set for a tag, or an empty string if none. +func (t Tags) Get(name string) string { + if t != nil { + if x := t[name]; len(x) >= 0 { + return x[0] + } + } + return "" +} + +// GetAll gets a copy of all values set for a tag, or nil if none. +func (t Tags) GetAll(name string) []string { + if t != nil { + if x := t[name]; len(x) >= 0 { + return slices.Clone(x) + } + } + return nil +} + +// Has checks if the specified tag contains one of the provided values. If no +// values are provided, it checks if any value is set. +func (t Tags) Has(name string, value ...string) bool { + if t != nil { + for _, x := range t[name] { + if len(value) == 0 { + return true + } + for _, value := range value { + if x == value { + return true + } + } + } + } + return false +} + +// HasFunc checks if the specified tag contains one of the provided values, +// using a function to compare it. +func (t Tags) HasFunc(name string, fn func(string) bool) bool { + if t != nil { + for _, x := range t[name] { + if fn != nil && fn(x) { + return true + } + } + } + return false +} + +// tagMut contains information about a mutation for [Tags]. +type tagMut struct { + Reset bool + Remove bool + Name string + Value string +} + +// parseTagMut parses a tag mutation in one of the forms: +// +// -name +// name +// name += value to add +// name -= value to remove +// name := value to replace +// +// The form "name +=" is identical to "name". +func parseTagMut(s string) (tagMut, error) { + var m tagMut + if k, v, ok := strings.Cut(s, "="); ok { + opi, op := 0, ' ' + for i, x := range k { + opi, op = i, x + } + switch op { + case '+': + // nothing + case ':': + m.Reset = true + case '-': + m.Remove = true + default: + return m, fmt.Errorf("mutation %q has invalid operator %c=", s, op) + } + m.Name = strings.TrimSpace(k[:opi]) + m.Value = strings.TrimSpace(v) + } else { + if m.Name, m.Reset = strings.CutPrefix(strings.TrimSpace(k), "-"); m.Reset { + m.Remove = true + } + } + if m.Name == "" || strings.ContainsFunc(m.Name, func(r rune) bool { + return !(('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') || ('0' <= r && r <= '9') || r == '_') + }) { + return m, fmt.Errorf("mutation %q has a invalid key %q", s, m.Name) + } + return m, nil +} + +// apply updates t with v. +func (v tagMut) Apply(t Tags) { + if v.Name == "" { + return + } + + // get the old slice from the map if we have a key + var ( + old = t[v.Name] + new = old + ) + + // if we're resetting or removing, clear the slice without deallocating it + if v.Reset || v.Remove { + new = old[:0] + + // if we're not resetting and just removing, add back the values + // which don't match + if !v.Reset { + for _, x := range old { + if x != v.Value { + new = append(new, x) + } + } + } + } + + // if we're not removing, add the new value + if !v.Remove { + new = append(new, v.Value) + } + + // update the map + t[v.Name] = new +} From b69123b414b22749840d48eb4b85f4200c29fd2a Mon Sep 17 00:00:00 2001 From: pg9182 <96569817+pg9182@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:07:39 -0400 Subject: [PATCH 2/3] pkg/api/api0: Add rules to request context --- pkg/api/api0/api.go | 43 +++++++++++++++++++++++++++++++++++++++++ pkg/api/api0/metrics.go | 2 ++ 2 files changed, 45 insertions(+) diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index 2e3593c..c3f819b 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -14,6 +14,7 @@ package api0 import ( "bytes" + "context" "crypto/rand" "encoding/hex" "encoding/json" @@ -30,6 +31,7 @@ import ( "github.com/r2northstar/atlas/pkg/eax" "github.com/r2northstar/atlas/pkg/metricsx" "github.com/r2northstar/atlas/pkg/nspkt" + "github.com/r2northstar/atlas/pkg/nsrule" "github.com/r2northstar/atlas/pkg/origin" "github.com/rs/zerolog/hlog" "golang.org/x/mod/semver" @@ -110,6 +112,9 @@ type Handler struct { // empty region and no error if no region is to be assigned. GetRegion func(netip.Addr, ip2x.Record) (string, error) + // Rules provides a ruleset to apply. + Rules *nsrule.RuleSet + metricsInit sync.Once metricsObj apiMetrics @@ -127,6 +132,14 @@ type connectState struct { gotPdata atomic.Bool } +type rulesContextKey struct{} + +type rulesContextValue struct { + e nsrule.Env + s *nsrule.RuleSet + t nsrule.Tags +} + // ServeHTTP routes requests to Handler. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var notPanicked bool // this lets us catch panics without swallowing them @@ -136,6 +149,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } }() + if h.Rules != nil { + r = r.WithContext(context.WithValue(r.Context(), rulesContextKey{}, &rulesContextValue{ + e: nsrule.NewEnv(), + s: h.Rules, + t: make(nsrule.Tags), + })) + h.EvalRules(r, nil) + } + w.Header().Set("Server", "Atlas") switch r.URL.Path { @@ -174,6 +196,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { notPanicked = true } +func (h *Handler) EvalRules(r *http.Request, update func(e nsrule.Env)) { + if v := r.Context().Value(rulesContextKey{}); v != nil { + t := time.Now() + v := v.(*rulesContextValue) + if update != nil { + update(v.e) + } + for _, err := range v.s.Evaluate(v.e, v.t) { + hlog.FromRequest(r).Warn().Err(err).Msg("failed to evaluate rule") + } + h.m().rule_evaluation_time_seconds.UpdateDuration(t) + } +} + +func (h *Handler) Tags(r *http.Request) nsrule.Tags { + if v := r.Context().Value(rulesContextKey{}); v != nil { + return v.(*rulesContextValue).t + } + return nsrule.Tags{} +} + // CheckLauncherVersion checks if the r was made by NorthstarLauncher and if it // is at least MinimumLauncherVersion. func (h *Handler) CheckLauncherVersion(r *http.Request, client bool) bool { diff --git a/pkg/api/api0/metrics.go b/pkg/api/api0/metrics.go index ac54944..9e15ee6 100644 --- a/pkg/api/api0/metrics.go +++ b/pkg/api/api0/metrics.go @@ -184,6 +184,7 @@ type apiMetrics struct { fail_other_error *metrics.Counter http_method_not_allowed *metrics.Counter } + rule_evaluation_time_seconds *metrics.Histogram } func (h *Handler) Metrics() *metrics.Set { @@ -474,6 +475,7 @@ func (h *Handler) m() *apiMetrics { mo.player_pdata_requests_total.fail_pdata_invalid = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_pdata_invalid"}`) mo.player_pdata_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_other_error"}`) mo.player_pdata_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="http_method_not_allowed"}`) + mo.rule_evaluation_time_seconds = mo.set.NewHistogram(`atlas_api0_rule_evaluation_time_seconds`) }) // ensure we initialized everything From 27f389075161206ca9f9ffc96328031f365de396 Mon Sep 17 00:00:00 2001 From: pg9182 <96569817+pg9182@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:07:40 -0400 Subject: [PATCH 3/3] pkg/atlas: Load rulesets --- pkg/atlas/config.go | 3 +++ pkg/atlas/server.go | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/pkg/atlas/config.go b/pkg/atlas/config.go index ed108c1..8089561 100644 --- a/pkg/atlas/config.go +++ b/pkg/atlas/config.go @@ -90,6 +90,9 @@ type Config struct { // 192.168.0.0/24=1.2.3.4). DevMapIP []string `env:"ATLAS_DEV_MAP_IP"` + // The path to a directory containing lexically-sorted rulesets. + Rules string `env:"ATLAS_RULES"` + // The maximum number of gameservers to allow. If -1, no limit is applied. API0_MaxServers int `env:"ATLAS_API0_MAX_SERVERS=1000"` diff --git a/pkg/atlas/server.go b/pkg/atlas/server.go index a713af8..d6a358f 100644 --- a/pkg/atlas/server.go +++ b/pkg/atlas/server.go @@ -29,6 +29,7 @@ import ( "github.com/r2northstar/atlas/pkg/eax" "github.com/r2northstar/atlas/pkg/memstore" "github.com/r2northstar/atlas/pkg/nspkt" + "github.com/r2northstar/atlas/pkg/nsrule" "github.com/r2northstar/atlas/pkg/origin" "github.com/r2northstar/atlas/pkg/regionmap" "github.com/rs/zerolog" @@ -50,6 +51,7 @@ type Server struct { API0 *api0.Handler Middleware []func(http.Handler) http.Handler TLSConfig *tls.Config + Rules *nsrule.RuleSet reload []func() closed bool @@ -78,6 +80,26 @@ func NewServer(c *Config) (*Server, error) { s.NotifySocket = c.NotifySocket + if c.Rules != "" { + s.Rules = new(nsrule.RuleSet) + if p, err := filepath.Abs(c.Rules); err == nil { + var err1 error + reload := func() { + if err := s.Rules.LoadFS(os.DirFS(p)); err != nil { + s.Logger.Err(err).Msgf("failed to load rules from %q", p) + err1 = fmt.Errorf("parse rulesets from %q: %w", p, err) + return + } + } + if reload(); err1 != nil { + return nil, fmt.Errorf("initialize rules: %w", err1) + } + s.reload = append(s.reload, reload) + } else { + return nil, fmt.Errorf("initialize rules: resolve path: %w", err) + } + } + if c.Web != "" { if p, err := filepath.Abs(c.Web); err == nil { var redirects sync.Map @@ -285,6 +307,7 @@ func NewServer(c *Config) (*Server, error) { MinimumLauncherVersionServer: c.API0_MinimumLauncherVersionServer, TokenExpiryTime: c.API0_TokenExpiryTime, AllowGameServerIPv6: c.API0_AllowGameServerIPv6, + Rules: s.Rules, } if v := c.API0_MinimumLauncherVersion; v != "" { if s.API0.MinimumLauncherVersionClient == "" {