package hostguard import ( "bytes" "encoding/json" "errors" "fmt" "io" "github.com/carlosframework/platform/internal/controlname" ) var ( ErrUnsupported = errors.New("hostguard: unsupported platform") ErrNotProvisioned = errors.New("hostguard: directory not provisioned") ErrUnsafeFile = errors.New("hostguard: expected regular file") ErrInvalidHost = errors.New("hostguard: invalid host") ErrHostTooLong = errors.New("hostguard: host too long for record or lock filename") ErrNoRecord = errors.New("hostguard: no local record") ErrMalformedRecord = errors.New("hostguard: malformed local record") ) type Captured struct { Incarnation string Stamp uint64 } type record struct { migrationDenies []MigrationDeny quarantined bool // settled marks a quarantine whose only cause is root's terminal // resolution of a pending intent: an obsolete binding (roster binding // N3) or a claim proven to have superseded it. It never lifts the // quarantine's execution refusal; the refusal-only migration census // alone may inspect such an installation. settled bool root bool installation RootInstallation rootRetired []Captured rebindCommit []byte pendingInventory []byte pendingInstallation *RootInstallation barrier *FenceTarget V int `json:"v"` Host string `json:"host"` Incarnation string `json:"incarnation"` ActiveStamp uint64 `json:"active_stamp"` RetiredStamps []uint64 `json:"retired_stamps"` ActiveFence *FenceTarget `json:"active_fence,omitempty"` FenceJournal []FenceReceipt `json:"fence_journal,omitempty"` } func (r record) validate(host string) error { if r.V != 1 || r.Host != host || !controlname.ValidHostShape(host) || r.Incarnation == "" || (!r.root && len(r.RetiredStamps) > 32) { return ErrMalformedRecord } var previous uint64 for _, s := range r.RetiredStamps { if s == 0 || s <= previous || (r.ActiveStamp != 0 && s >= r.ActiveStamp) { return ErrMalformedRecord } previous = s } if r.ActiveFence != nil { if err := r.ActiveFence.validate(host); err != nil { return err } } for i := range r.FenceJournal { if err := r.FenceJournal[i].validate(host); err != nil { return err } if i > 0 && r.FenceJournal[i-1].Target == r.FenceJournal[i].Target { return ErrMalformedRecord } } return nil } func validateFileHost(host string) error { if !controlname.ValidHostShape(host) { return ErrInvalidHost } if len(host)+len(".json") > 255 || len(host)+len(".lock") > 255 { return ErrHostTooLong } return nil } func readRecord(dataDir, host string) (record, error) { if err := validateFileHost(host); err != nil { return record{}, err } d, err := openRecordDir(dataDir) if err != nil { return record{}, err } defer d.close() return d.read(host) } func decodeRecord(b []byte, host string) (record, error) { d := json.NewDecoder(bytes.NewReader(b)) d.DisallowUnknownFields() var r record if err := d.Decode(&r); err != nil { return record{}, fmt.Errorf("%w: %v", ErrMalformedRecord, err) } if _, err := d.Token(); err != io.EOF { return record{}, ErrMalformedRecord } if err := r.validate(host); err != nil { return record{}, err } return r, nil } func Capture(dataDir, host string) (Captured, error) { r, err := readRecord(dataDir, host) if err != nil { return Captured{}, err } return Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp}, nil } func writeRecord(dataDir string, r record) error { if err := validateFileHost(r.Host); err != nil { return err } if err := r.validate(r.Host); err != nil { return err } d, err := openRecordDir(dataDir) if err != nil { return err } defer d.close() return d.write(r) }