carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "io"
10 "log"
11 "os"
12 "path/filepath"
13 "reflect"
14 "strings"
15
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/rootstate"
19)
20
21type namedUnitRecovery struct {
22 attempts func() (rootstate.Store, error)
23 history func() (*release.RootHistoryPass, error)
24 source *proofBindingSource
25 pub ed25519.PublicKey
26 ratchet *release.DigestRatchet
27 writes hostwrite.Config
28 readLive func(string) (string, error)
29}
30
31func productionNamedUnitRecovery(store release.Store, pub ed25519.PublicKey, optDir string, writes hostwrite.Config) namedUnitRecovery {
32 return namedUnitRecovery{source: productionProofBindingSource(store, systemdUnits{}, optDir), pub: pub,
33 ratchet: pointerRatchet(true, optDir, dataDir(), os.Getenv), writes: writes,
34 readLive: rootstate.ReadTrustedLink,
35 }
36}
37
38func (n namedUnitRecovery) repair(ctx context.Context, host string) (bool, error) {
39 if n.source == nil || n.readLive == nil {
40 return false, os.ErrPermission
41 }
42 rec, _, err := n.source.findRecord(ctx, host)
43 if err != nil {
44 return false, err
45 }
46 if rec.Unit == "" {
47 return false, nil
48 }
49 rec, binding, props, err := n.source.unitBinding(ctx, host)
50 if err != nil {
51 return true, err
52 }
53 if props.ActiveState == "active" && props.MainPID > 0 {
54 return false, nil
55 }
56 if props.MainPID != 0 || (props.ActiveState != "failed" && props.ActiveState != "inactive") {
57 return true, os.ErrPermission
58 }
59 live := release.LiveLink(n.source.optDir, host)
60 before, err := n.readLive(live)
61 if err != nil {
62 return true, err
63 }
64 version, err := release.LiveVersion(n.source.optDir, host)
65 if err != nil || !release.ValidVersion(version) || before != release.VersionPath(n.source.optDir, rec.Account, version, rec.App) {
66 return true, os.ErrPermission
67 }
68 pointer, m, err := release.StageSignedHead(ctx, n.source.store, n.pub, n.ratchet, n.source.optDir, binding.Route)
69 if err != nil {
70 return true, err
71 }
72 work, err := n.writes.Select(ctx, host, "release")
73 if err != nil {
74 return true, err
75 }
76 id := hostwrite.Identity{}
77 if n.writes.Enabled {
78 raw, err := json.Marshal(m)
79 if err != nil {
80 return true, err
81 }
82 digest := sha256.Sum256(raw)
83 id = hostwrite.Identity{Release: "sha256:" + hex.EncodeToString(digest[:])}
84 }
85 err = work.With(ctx, id, func() error {
86 _, current, state, err := n.source.unitBinding(ctx, host)
87 if err != nil {
88 return err
89 }
90 if current.Source != binding.Source || state.MainPID != 0 || (state.ActiveState != "failed" && state.ActiveState != "inactive") {
91 return os.ErrPermission
92 }
93 fresh, err := release.Channel(ctx, n.source.store, n.pub, rec.Account, rec.App, binding.Route.Channel)
94 if err != nil {
95 return err
96 }
97 if !reflect.DeepEqual(fresh, pointer) {
98 return os.ErrPermission
99 }
100 if err := n.checkHistory(binding, pointer); err != nil {
101 return err
102 }
103 still, err := n.readLive(live)
104 if err != nil || still != before {
105 return os.ErrPermission
106 }
107 exe, err := n.source.openFile(release.VersionPath(n.source.optDir, rec.Account, pointer.Version, rec.App))
108 if err != nil {
109 return err
110 }
111 hash := sha256.New()
112 _, hashErr := io.Copy(hash, exe)
113 exe.Close()
114 if hashErr != nil {
115 return hashErr
116 }
117 if hex.EncodeToString(hash.Sum(nil)) != m.Artifacts[release.AdoptPlatform()].SHA256 {
118 return os.ErrPermission
119 }
120 if err := release.PointLive(n.source.optDir, host, rec.Account, pointer.Version, rec.App); err != nil {
121 return err
122 }
123 dir, err := os.Open(filepath.Dir(live))
124 if err != nil {
125 return err
126 }
127 defer dir.Close()
128 return dir.Sync()
129 })
130 return true, err
131}
132
133func (n namedUnitRecovery) checkHistory(b proofBinding, p *release.ChannelPointer) error {
134 binding := release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App}
135 var history *release.RootHistory
136 if n.history != nil {
137 h, err := n.history()
138 if err == nil && h != nil && h.History != nil && h.History.Store != nil {
139 history = h.History
140 defer history.Store.Close()
141 }
142 }
143 if n.originalRefusal(history, binding, p) {
144 return os.ErrPermission
145 }
146 blocked, err := history.Blocks(binding, p)
147 if err != nil {
148 return err
149 }
150 if blocked {
151 return os.ErrPermission
152 }
153 if history != nil {
154 if err := history.RetireD1(binding, p); err != nil {
155 log.Print(err)
156 }
157 }
158 return nil
159}
160
161func (n namedUnitRecovery) originalRefusal(h *release.RootHistory, b release.HistoryBinding, p *release.ChannelPointer) bool {
162 if n.attempts == nil {
163 return false
164 }
165 s, err := n.attempts()
166 if err != nil {
167 return false
168 }
169 defer s.Close()
170 names, _ := s.Names()
171 for _, name := range names {
172 if !strings.HasSuffix(name, ".recovery") && !strings.HasSuffix(name, ".attempt") {
173 continue
174 }
175 raw, err := s.Read(name)
176 var a rollbackAttempt
177 if err != nil || len(raw) > rootstate.MaxRecord || json.Unmarshal(raw, &a) != nil || (name != rollbackJournalKey(a.Host) && name != recoveryResultKey(a)) {
178 continue
179 }
180 r, err := recoveryHistoryOutcome(a)
181 if err != nil || r.Binding != b || (!r.Failed && !r.Recovering) {
182 continue
183 }
184 ref := release.Refusal{Version: r.Attempt.Version, PointerPromotedAt: r.Attempt.PromotedAt}
185 if !ref.Blocks(p) {
186 continue
187 }
188 if h != nil && r.Settled {
189 retired, err := h.RecoveryRetired(r)
190 if err == nil && retired {
191 continue
192 }
193 }
194 return true
195 }
196 return false
197}
198