carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "encoding/json"
5 "io"
6 "net/http"
7 "time"
8
9 "github.com/carlosframework/platform/internal/hostactors"
10 "github.com/carlosframework/platform/internal/hostclaim"
11 "github.com/carlosframework/platform/internal/hostfence"
12 "github.com/carlosframework/platform/internal/hostfloor"
13 "github.com/carlosframework/platform/internal/hostops"
14 "github.com/carlosframework/platform/internal/hostresolve"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18const claimHostResolveUnsupportedMsg = "host-loss resolution is unavailable until its lifecycle authority is connected"
19
20const hostClaimUnavailable = "host claim authority is unavailable"
21
22func (s *Server) hostClaimOperator(w http.ResponseWriter, r *http.Request) (string, bool) {
23 addr, ok := s.cliUser(w, r)
24 if !ok {
25 return "", false
26 }
27 yes, err := s.store.IsOperator(addr)
28 if err != nil || !yes || !s.cfg.HostClaim {
29 http.NotFound(w, r)
30 return "", false
31 }
32 if s.cfg.Bucket == "" || len(s.cfg.Key) == 0 || s.cfg.Releases == nil {
33 jsonError(w, http.StatusServiceUnavailable, hostClaimUnavailable)
34 return "", false
35 }
36 if _, err := hostfloor.Read(r.Context(), s.cfg.Releases, release.RootSigner(s.cfg.Key).RootPublicKey(), s.cfg.Bucket); err != nil {
37 jsonError(w, http.StatusServiceUnavailable, hostClaimUnavailable)
38 return "", false
39 }
40 return addr, true
41}
42func (s *Server) hostResolver() hostresolve.Service {
43 signer := release.RootSigner(s.cfg.Key)
44 return hostresolve.Service{Directory: hostactors.Directory{Store: s.cfg.Releases, PublicKey: signer.RootPublicKey(), Bucket: s.cfg.Bucket}, Signer: signer, Now: time.Now}
45}
46func (s *Server) handleAPIHostResolveInspect(w http.ResponseWriter, r *http.Request) {
47 actor, ok := s.hostClaimOperator(w, r)
48 if !ok {
49 return
50 }
51 var in struct {
52 Action string `json:"action"`
53 Box string `json:"box"`
54 }
55 if json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&in) != nil {
56 jsonError(w, 400, "invalid resolution request")
57 return
58 }
59 if in.Action == "declare-lost" {
60 if account, app, ok := s.claimMoveLossHolder(r, r.PathValue("host")); ok {
61 inspected, err := s.cfg.ClaimAuthority.InspectMoveLoss(r.Context(), actor, account, app, r.PathValue("host"), in.Box)
62 if err != nil {
63 writeClaimResponseError(w, err)
64 return
65 }
66 json.NewEncoder(w).Encode(inspected)
67 return
68 }
69 }
70 svc := s.hostResolver()
71 if in.Action == "readmit" {
72 ref, b, err := svc.Directory.Resolve(r.Context(), in.Box)
73 if err != nil {
74 writeClaimResponseError(w, err)
75 return
76 }
77 json.NewEncoder(w).Encode(map[string]any{"box": ref, "enrollment": b.Enrollment, "revision": b.QuarantineRevision, "loss_ids": b.LossIDs})
78 return
79 }
80 if in.Action != "declare-lost" {
81 jsonError(w, 400, "invalid resolution action")
82 return
83 }
84 inspected, err := svc.Inspect(r.Context(), actor, r.PathValue("host"), in.Box)
85 if err != nil {
86 writeClaimResponseError(w, err)
87 return
88 }
89 json.NewEncoder(w).Encode(inspected)
90}
91func (s *Server) handleAPIHostResolve(w http.ResponseWriter, r *http.Request) {
92 actor, ok := s.hostClaimOperator(w, r)
93 if !ok {
94 return
95 }
96 var in struct {
97 Action string `json:"action"`
98 Submission hostfence.Submission `json:"submission"`
99 Box string `json:"box"`
100 Enrollment string `json:"enrollment"`
101 Revision uint64 `json:"revision"`
102 LossIDs []string `json:"loss_ids"`
103 }
104 var raw json.RawMessage
105 decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
106 if decoder.Decode(&raw) != nil || json.Unmarshal(raw, &in) != nil {
107 jsonError(w, 400, "invalid resolution request")
108 return
109 }
110 if in.Action == "reset-wake-retry" {
111 if decoder.Decode(new(any)) != io.EOF {
112 w.WriteHeader(http.StatusBadRequest)
113 return
114 }
115 s.handleHostWakeRetryReset(w, r, actor, raw)
116 return
117 }
118 // A move's target loss is the moves owner's, under the typed
119 // ControlWriter's scoped declaration (claim Plan 10, J4 and J17). The box
120 // is the signed challenge's.
121 if in.Action == "declare-lost" {
122 if account, app, ok := s.claimMoveLossHolder(r, r.PathValue("host")); ok {
123 if err := s.declareMoveLossLogged(r.Context(), account, app, r.PathValue("host"), in.Submission, func() error {
124 return s.cfg.ClaimAuthority.DeclareMoveLoss(r.Context(), actor, account, app, r.PathValue("host"), s.claimChallengeBox(in.Submission), in.Submission)
125 }); err != nil {
126 writeClaimResponseError(w, err)
127 return
128 }
129 s.bumpSerial(r.Context())
130 json.NewEncoder(w).Encode(map[string]bool{"ok": true})
131 return
132 }
133 }
134 // This service changes claim roots; an ordinary app permit is not its
135 // authority. Every other resolution is refused before acceptance or
136 // quarantine until its owning lifecycle protocol supplies a scoped writer.
137 if s.cfg.HostClaim {
138 jsonError(w, http.StatusConflict, claimHostResolveUnsupportedMsg)
139 return
140 }
141 svc := s.hostResolver()
142 if in.Action == "readmit" {
143 if err := svc.Readmit(r.Context(), in.Box, in.Enrollment, in.Revision, in.LossIDs); err != nil {
144 jsonError(w, 409, err.Error())
145 return
146 }
147 } else if in.Action == "declare-lost" {
148 if _, err := svc.Declare(r.Context(), actor, r.PathValue("host"), in.Submission); err != nil {
149 jsonError(w, 409, err.Error())
150 return
151 }
152 } else {
153 jsonError(w, 400, "invalid resolution action")
154 return
155 }
156 json.NewEncoder(w).Encode(map[string]bool{"ok": true})
157}
158
159// claimMoveLossHolder answers the holder's account and app when host's claim
160// is under a move kind the moves owner resolves, on a console with the moves
161// switch and its claim runtime. Every other host keeps the existing path.
162func (s *Server) claimMoveLossHolder(r *http.Request, host string) (string, string, bool) {
163 a := s.cfg.ClaimAuthority
164 if !s.cfg.HostClaim || !s.cfg.MovesEnabled || a == nil || a.Control == nil {
165 return "", "", false
166 }
167 o, err := hostclaim.Read(r.Context(), s.cfg.Releases, host)
168 if err != nil || o.Absent || o.Claim == nil || o.Claim.Holder == nil || !hostops.MoveLossKind(o.Claim.Op.Kind) {
169 return "", "", false
170 }
171 return o.Claim.Holder.Account, o.Claim.Holder.App, true
172}
173
174// claimChallengeBox is the box reference the submission's signed challenge
175// names, or "" when it does not verify. DeclareMoveLoss verifies and
176// compares it again.
177func (s *Server) claimChallengeBox(in hostfence.Submission) string {
178 var c hostfence.Challenge
179 if release.VerifyRootEnvelope(s.cfg.ClaimAuthority.Directory.PublicKey, in.Challenge, &c) != nil {
180 return ""
181 }
182 return c.Box
183}
184