package server import ( "encoding/json" "errors" "fmt" "net/http" "sort" "strconv" "strings" "housecarl/internal/api" "housecarl/internal/engine" "housecarl/internal/model" ) // Every configuration change is an operation (model.Op): applied at once, // or staged in a changeset and replayed when the changeset is applied. // applyOp is the one place an operation becomes an edit. // applyOp makes an operation's change to st. It returns the ID of the // object it made or changed (0 for deletes and settings). func applyOp(st *model.State, op *model.Op) (int64, error) { switch op.Op { case model.OpCreate: return createOp(st, op) case model.OpUpdate: return updateOp(st, op) case model.OpDelete: return 0, deleteOp(st, op) case model.OpMove: return moveOp(st, op) case model.OpSettings: return 0, settingsOp(st, op) case model.OpRevert: var h model.HistoryEntry if err := json.Unmarshal(op.Data, &h); err != nil { return 0, badRequest("revert: %v", err) } return 0, engine.Undo(st, &h) } return 0, badRequest("unknown operation %q", op.Op) } func createOp(st *model.State, op *model.Op) (int64, error) { o, err := decodeObject(op.Kind, op.Data) if err != nil { return 0, err } if svc, ok := o.(*model.Service); ok { svc.Builtin = false } if rule, ok := o.(*model.Rule); ok && rule.Action == "" { rule.Action = model.ActionAccept } if f, ok := o.(*model.PortForward); ok && len(f.Protocols) == 0 { f.Protocols = []string{"tcp"} } if st.ByID(op.Kind, op.ID) != nil { return 0, conflict("%s %d exists already", op.Kind, op.ID) } setID(o, op.ID) if pos := position(o); pos != nil && *pos == 0 { *pos = lastPosition(st, op.Kind, chainOf(o)) } data, _ := json.Marshal(o) if err := st.Put(op.Kind, op.ID, data); err != nil { return 0, err } // Placed in the same change, so it is never in force anywhere else. if len(op.Place) > 0 { if _, err := moveOp(st, &model.Op{Op: model.OpMove, Kind: op.Kind, Ref: strconv.FormatInt(op.ID, 10), Data: op.Place}); err != nil { return 0, err } } return op.ID, nil } func updateOp(st *model.State, op *model.Op) (int64, error) { var patch map[string]json.RawMessage if err := json.Unmarshal(op.Data, &patch); err != nil { return 0, badRequest("%s: %v", op.Kind, err) } delete(patch, "id") o, err := findObject(st, op.Kind, op.Ref) if err != nil { return 0, err } id := o.GetID() // The patch's fields over the object's. var cur map[string]json.RawMessage b, _ := json.Marshal(o) json.Unmarshal(b, &cur) for k, v := range patch { if string(v) == "null" { delete(cur, k) } else { cur[k] = v } } merged, _ := json.Marshal(cur) next, err := decodeObject(op.Kind, merged) if err != nil { return 0, err } setID(next, id) if old, now := o.Label(), next.Label(); old != now && isNamed(op.Kind) { if set, ok := o.(*model.AddressSet); ok && set.Dynamic { return 0, conflict("set %s is dynamic: its entries, and the API tokens and programs that fill it, go by its name; make a new set instead", old) } renameRefs(st, op.Kind, old, now) } data, _ := json.Marshal(next) return id, st.Put(op.Kind, id, data) } // isNamed reports whether other objects refer to a kind by name. func isNamed(kind string) bool { return kind == model.KindZone || kind == model.KindSet || kind == model.KindService } func deleteOp(st *model.State, op *model.Op) error { o, err := findObject(st, op.Kind, op.Ref) if err != nil { return err } if used := usedBy(st, op.Kind, o); used != "" { return conflict("%s %s is used by %s; change that first", op.Kind, o.Label(), used) } return st.Put(op.Kind, o.GetID(), nil) } // moveOp reorders a rule or NAT rule: before or after another, or to the // start or end; a rule can change chain at the same time. func moveOp(st *model.State, op *model.Op) (int64, error) { var in api.MoveInput if err := json.Unmarshal(op.Data, &in); err != nil { return 0, badRequest("move: %v", err) } o, err := findObject(st, op.Kind, op.Ref) if err != nil { return 0, err } id := o.GetID() pos := position(o) if pos == nil { return 0, badRequest("only rules, port forwards, source NAT rules and redirects have an order") } if rule, ok := o.(*model.Rule); ok && in.Chain != "" { rule.Chain = in.Chain } chain := chainOf(o) // The others in order. var others []model.Object for _, x := range st.Objects(op.Kind) { if x.GetID() != id && chainOf(x) == chain { others = append(others, x) } } sort.SliceStable(others, func(i, j int) bool { return *position(others[i]) < *position(others[j]) }) place := func(i int) { // before others[i] (i == len: at the end) switch { case len(others) == 0: *pos = 1 case i == 0: *pos = *position(others[0]) - 1 case i == len(others): *pos = *position(others[len(others)-1]) + 1 default: *pos = (*position(others[i-1]) + *position(others[i])) / 2 } } index := func(ref int64) (int, error) { for i, x := range others { if x.GetID() == ref { return i, nil } } return 0, badRequest("%d isn't another %s of the same chain", ref, op.Kind) } switch { case in.Before != 0: i, err := index(in.Before) if err != nil { return 0, err } place(i) case in.After != 0: i, err := index(in.After) if err != nil { return 0, err } place(i + 1) case in.To == "start": place(0) case in.To == "end" || in.Chain != "": place(len(others)) default: return 0, badRequest("say where: before, after, or to start or end") } data, _ := json.Marshal(o) return id, st.Put(op.Kind, id, data) } func settingsOp(st *model.State, op *model.Op) error { var patch map[string]json.RawMessage if err := json.Unmarshal(op.Data, &patch); err != nil { return badRequest("settings: %v", err) } var cur map[string]json.RawMessage b, _ := json.Marshal(st.Settings) json.Unmarshal(b, &cur) for k, v := range patch { // Built-ins merge one by one. if k == "builtins" { var cb, pb map[string]json.RawMessage json.Unmarshal(cur[k], &cb) if err := json.Unmarshal(v, &pb); err != nil { return badRequest("builtins: %v", err) } for bk, bv := range pb { cb[bk] = bv } v, _ = json.Marshal(cb) } cur[k] = v } merged, _ := json.Marshal(cur) var next model.Settings dec := json.NewDecoder(strings.NewReader(string(merged))) dec.DisallowUnknownFields() if err := dec.Decode(&next); err != nil { return badRequest("settings: %v", err) } st.Settings = next return nil } // replay applies a changeset's operations to st, in order. An operation // that fails is named in the error (opError). func replay(st *model.State, ops []model.Op) error { for i := range ops { if _, err := applyOp(st, &ops[i]); err != nil { return &opError{seq: ops[i].Seq, summary: ops[i].Summary, err: err} } } return nil } // opError: an operation of a changeset that can't be applied (the // objects it names changed since it was staged, say). type opError struct { seq int summary string err error } func (e *opError) Error() string { return fmt.Sprintf("operation %d (%s) can't be applied: %v", e.seq, e.summary, e.err) } func (e *opError) Unwrap() error { return e.err } // info is the error for the API, with the inner error's status. func (e *opError) info() (api.ErrorInfo, int) { info := api.ErrorInfo{Code: "conflict", Message: e.Error(), Op: e.seq} status := http.StatusConflict var he *httpError if errors.As(e.err, &he) { info.Code, status = he.code, he.status } if ee, ok := engine.AsError(e.err); ok { info.Code, info.Problems, status = ee.Kind, ee.Problems, engineStatus(ee.Kind) } return info, status } // objectOf picks what an operation's answer returns from a state. func objectOf(op *model.Op, id int64) func(st *model.State) any { switch op.Op { case model.OpDelete: return nil case model.OpSettings: return func(st *model.State) any { return st.Settings } } return func(st *model.State) any { if o := st.ByID(op.Kind, id); o != nil { return o } return nil } }