package server import ( "encoding/json" "net/http" "slices" "strconv" "jarl/internal/api" "jarl/internal/auth" "jarl/internal/engine" "jarl/internal/model" ) // Blocking steps and client groups (docs/blocking.md). func presentBlockStep(st *model.State, s *model.BlockStep) api.BlockStep { out := api.BlockStep{BlockStep: *s, Lists: []string{}, Groups: []string{}} for _, b := range st.SortedBlocklists() { if b.StepID == s.ID { out.Lists = append(out.Lists, b.Name) } } for _, g := range st.SortedClientGroups() { if g.StepID == s.ID { out.Groups = append(out.Groups, g.Name) } } return out } func presentClientGroup(st *model.State, g *model.ClientGroup) api.ClientGroup { return api.ClientGroup{ClientGroup: *g, Step: st.StepName(g.StepID), Views: viewNames(st, st.BlockingViews(g.ViewIDs)), AutoViews: len(g.ViewIDs) == 0 && len(st.Views) > 0} } func stepObject(st *model.State, id int64) any { if s := st.BlockSteps[id]; s != nil { return presentBlockStep(st, s) } return nil } func groupObject(st *model.State, id int64) any { if g := st.ClientGroups[id]; g != nil { return presentClientGroup(st, g) } return nil } func (s *Server) listBlockSteps(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireBlocking(p); err != nil { return err } st, base, err := s.states(r) if err != nil { return err } out := []api.BlockStep{} for _, x := range st.OrderedBlockSteps() { o := presentBlockStep(st, x) o.Pending = pending(base, base != nil && base.BlockSteps[x.ID] == nil, base != nil && base.BlockSteps[x.ID] != nil && base.BlockSteps[x.ID].Version != x.Version) out = append(out, o) } if base != nil { for _, x := range base.OrderedBlockSteps() { if st.BlockSteps[x.ID] == nil { o := presentBlockStep(base, x) o.Pending = "delete" out = append(out, o) } } } writeJSON(w, http.StatusOK, out) return nil } // stepByPath finds a step above normal by ID or name. func stepByPath(r *http.Request, st *model.State) (*model.BlockStep, error) { ref := r.PathValue("id") if id, err := strconv.ParseInt(ref, 10, 64); err == nil { if x := st.BlockSteps[id]; x != nil { return x, nil } } else if id, ok := st.StepByName(ref); ok && id > 0 { return st.BlockSteps[id], nil } return nil, notFound("blocking step %s not found (off and normal are built in)", ref) } func (s *Server) createBlockStep(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { body, err := readBody(r) if err != nil { return err } return s.mutate(w, r, p, "", []engine.Request{request(engine.OpBlockStepCreate, 0, body)}, stepObject) } func (s *Server) updateBlockStep(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { st, err := s.state(r) if err != nil { return err } x, err := stepByPath(r, st) if err != nil { return err } body, err := readBody(r) if err != nil { return err } id := x.ID return s.mutate(w, r, p, "", []engine.Request{request(engine.OpBlockStepUpdate, id, body)}, func(st *model.State, _ int64) any { return stepObject(st, id) }) } func (s *Server) deleteBlockStep(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { st, err := s.state(r) if err != nil { return err } x, err := stepByPath(r, st) if err != nil { return err } return s.mutate(w, r, p, "", []engine.Request{request(engine.OpBlockStepDelete, x.ID, nil)}, nil) } // orderBlockSteps: POST /blocking/steps/order {"steps": ["strict", // "very-strict"]} (names or IDs as strings) sets the order of the steps // above normal, lowest first. func (s *Server) orderBlockSteps(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { st, err := s.state(r) if err != nil { return err } var in struct { Steps []string `json:"steps"` } if err := decodeJSON(r, &in); err != nil { return err } if len(in.Steps) != len(st.BlockSteps) { return badRequest("give every step above normal once (%d)", len(st.BlockSteps)) } var reqs []engine.Request seen := map[int64]bool{} for i, ref := range in.Steps { var x *model.BlockStep if id, err := strconv.ParseInt(ref, 10, 64); err == nil { x = st.BlockSteps[id] } else if id, ok := st.StepByName(ref); ok && id > 0 { x = st.BlockSteps[id] } if x == nil || seen[x.ID] { return badRequest("step %q doesn't exist or is given twice", ref) } seen[x.ID] = true if x.Position != i { data, _ := json.Marshal(engine.BlockStepInput{Position: model.Ptr(i)}) reqs = append(reqs, request(engine.OpBlockStepUpdate, x.ID, data)) } } if len(reqs) == 0 { writeJSON(w, http.StatusOK, map[string]string{"message": "the steps are already in that order"}) return nil } return s.mutate(w, r, p, "", reqs, nil) } func (s *Server) listClientGroups(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireBlocking(p); err != nil { return err } st, base, err := s.states(r) if err != nil { return err } out := []api.ClientGroup{} for _, g := range st.SortedClientGroups() { o := presentClientGroup(st, g) o.Pending = pending(base, base != nil && base.ClientGroups[g.ID] == nil, base != nil && base.ClientGroups[g.ID] != nil && base.ClientGroups[g.ID].Version != g.Version) out = append(out, o) } if base != nil { for _, g := range base.SortedClientGroups() { if st.ClientGroups[g.ID] == nil { o := presentClientGroup(base, g) o.Pending = "delete" out = append(out, o) } } } writeJSON(w, http.StatusOK, out) return nil } func groupByPath(r *http.Request, st *model.State) (*model.ClientGroup, error) { ref := r.PathValue("id") if id, err := strconv.ParseInt(ref, 10, 64); err == nil { if g := st.ClientGroups[id]; g != nil { return g, nil } } else if g := st.ClientGroupByName(ref); g != nil { return g, nil } return nil, notFound("client group %s not found", ref) } func (s *Server) createClientGroup(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { body, err := readBody(r) if err != nil { return err } return s.mutate(w, r, p, "", []engine.Request{request(engine.OpClientGroupCreate, 0, body)}, groupObject) } func (s *Server) updateClientGroup(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { st, err := s.state(r) if err != nil { return err } g, err := groupByPath(r, st) if err != nil { return err } body, err := readBody(r) if err != nil { return err } id := g.ID return s.mutate(w, r, p, "", []engine.Request{request(engine.OpClientGroupUpdate, id, body)}, func(st *model.State, _ int64) any { return groupObject(st, id) }) } func (s *Server) deleteClientGroup(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { st, err := s.state(r) if err != nil { return err } g, err := groupByPath(r, st) if err != nil { return err } return s.mutate(w, r, p, "", []engine.Request{request(engine.OpClientGroupDelete, g.ID, nil)}, nil) } // stepReached reports whether any client gets a list: always for a list at // normal; for one above it, when a client group with members in one of its // views is at its step or above. func stepReached(st *model.State, b *model.Blocklist, views []int64) bool { rank, _ := st.StepRank(b.StepID) if rank <= 0 { return true } for _, g := range st.ClientGroups { r, ok := st.StepRank(g.StepID) if !ok || r < rank || len(st.GroupPrefixes(g)) == 0 { continue } for _, v := range st.BlockingViews(g.ViewIDs) { if slices.Contains(views, v) { return true } } } return false }