package host import ( "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "io" "log" "os" "path/filepath" "reflect" "strings" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type namedUnitRecovery struct { attempts func() (rootstate.Store, error) history func() (*release.RootHistoryPass, error) source *proofBindingSource pub ed25519.PublicKey ratchet *release.DigestRatchet writes hostwrite.Config readLive func(string) (string, error) } func productionNamedUnitRecovery(store release.Store, pub ed25519.PublicKey, optDir string, writes hostwrite.Config) namedUnitRecovery { return namedUnitRecovery{source: productionProofBindingSource(store, systemdUnits{}, optDir), pub: pub, ratchet: pointerRatchet(true, optDir, dataDir(), os.Getenv), writes: writes, readLive: rootstate.ReadTrustedLink, } } func (n namedUnitRecovery) repair(ctx context.Context, host string) (bool, error) { if n.source == nil || n.readLive == nil { return false, os.ErrPermission } rec, _, err := n.source.findRecord(ctx, host) if err != nil { return false, err } if rec.Unit == "" { return false, nil } rec, binding, props, err := n.source.unitBinding(ctx, host) if err != nil { return true, err } if props.ActiveState == "active" && props.MainPID > 0 { return false, nil } if props.MainPID != 0 || (props.ActiveState != "failed" && props.ActiveState != "inactive") { return true, os.ErrPermission } live := release.LiveLink(n.source.optDir, host) before, err := n.readLive(live) if err != nil { return true, err } version, err := release.LiveVersion(n.source.optDir, host) if err != nil || !release.ValidVersion(version) || before != release.VersionPath(n.source.optDir, rec.Account, version, rec.App) { return true, os.ErrPermission } pointer, m, err := release.StageSignedHead(ctx, n.source.store, n.pub, n.ratchet, n.source.optDir, binding.Route) if err != nil { return true, err } work, err := n.writes.Select(ctx, host, "release") if err != nil { return true, err } id := hostwrite.Identity{} if n.writes.Enabled { raw, err := json.Marshal(m) if err != nil { return true, err } digest := sha256.Sum256(raw) id = hostwrite.Identity{Release: "sha256:" + hex.EncodeToString(digest[:])} } err = work.With(ctx, id, func() error { _, current, state, err := n.source.unitBinding(ctx, host) if err != nil { return err } if current.Source != binding.Source || state.MainPID != 0 || (state.ActiveState != "failed" && state.ActiveState != "inactive") { return os.ErrPermission } fresh, err := release.Channel(ctx, n.source.store, n.pub, rec.Account, rec.App, binding.Route.Channel) if err != nil { return err } if !reflect.DeepEqual(fresh, pointer) { return os.ErrPermission } if err := n.checkHistory(binding, pointer); err != nil { return err } still, err := n.readLive(live) if err != nil || still != before { return os.ErrPermission } exe, err := n.source.openFile(release.VersionPath(n.source.optDir, rec.Account, pointer.Version, rec.App)) if err != nil { return err } hash := sha256.New() _, hashErr := io.Copy(hash, exe) exe.Close() if hashErr != nil { return hashErr } if hex.EncodeToString(hash.Sum(nil)) != m.Artifacts[release.AdoptPlatform()].SHA256 { return os.ErrPermission } if err := release.PointLive(n.source.optDir, host, rec.Account, pointer.Version, rec.App); err != nil { return err } dir, err := os.Open(filepath.Dir(live)) if err != nil { return err } defer dir.Close() return dir.Sync() }) return true, err } func (n namedUnitRecovery) checkHistory(b proofBinding, p *release.ChannelPointer) error { binding := release.HistoryBinding{Host: b.Route.Host, Account: b.Route.Account, App: b.Route.App} var history *release.RootHistory if n.history != nil { h, err := n.history() if err == nil && h != nil && h.History != nil && h.History.Store != nil { history = h.History defer history.Store.Close() } } if n.originalRefusal(history, binding, p) { return os.ErrPermission } blocked, err := history.Blocks(binding, p) if err != nil { return err } if blocked { return os.ErrPermission } if history != nil { if err := history.RetireD1(binding, p); err != nil { log.Print(err) } } return nil } func (n namedUnitRecovery) originalRefusal(h *release.RootHistory, b release.HistoryBinding, p *release.ChannelPointer) bool { if n.attempts == nil { return false } s, err := n.attempts() if err != nil { return false } defer s.Close() names, _ := s.Names() for _, name := range names { if !strings.HasSuffix(name, ".recovery") && !strings.HasSuffix(name, ".attempt") { continue } raw, err := s.Read(name) var a rollbackAttempt if err != nil || len(raw) > rootstate.MaxRecord || json.Unmarshal(raw, &a) != nil || (name != rollbackJournalKey(a.Host) && name != recoveryResultKey(a)) { continue } r, err := recoveryHistoryOutcome(a) if err != nil || r.Binding != b || (!r.Failed && !r.Recovering) { continue } ref := release.Refusal{Version: r.Attempt.Version, PointerPromotedAt: r.Attempt.PromotedAt} if !ref.Blocks(p) { continue } if h != nil && r.Settled { retired, err := h.RecoveryRetired(r) if err == nil && retired { continue } } return true } return false }