//go:build linux package operator import ( "context" "os" "time" "amadan.net/carlos/platform.git/launchctx" "amadan.net/carlos/platform.git/launchctx/controlname" "amadan.net/oficina/home/internal/operatorlaunch" ) const commandConfigEnvironment = "HOME_OPERATOR_COMMAND" type commandConfig struct { Version int `json:"version"` Account string `json:"account"` App string `json:"app"` Host string `json:"host"` Role string `json:"role"` Origin string `json:"origin"` Installation string `json:"installation"` Registry string `json:"registry"` Device string `json:"device"` PublicKey string `json:"public_key"` KeyID string `json:"key_id"` Actions []string `json:"actions"` } func parseCommandConfig(raw string) (commandConfig, *Verifier, error) { var c commandConfig if len(raw) == 0 || len(raw) > 8192 || strict([]byte(raw), &c) != nil || c.Version != 1 || !controlname.ValidAccount(c.Account) || !controlname.ValidApp(c.App) || !controlname.ValidHostShape(c.Host) { return commandConfig{}, nil, ErrRefused } v, err := NewVerifier(Pins{c.Role, c.Origin, c.Installation, c.Registry, c.Device, c.PublicKey, c.Actions}) if err != nil || c.KeyID != v.kid { return commandConfig{}, nil, ErrRefused } return c, v, nil } // commandInvocation remains private to the unmounted command adapter. Nothing // in the HTTP app can supply a database, verifier or transport context to it. type commandInvocation struct { database *boundDatabase verifier *Verifier request Request launch launchctx.LaunchContext raw []byte ctx context.Context cancel context.CancelFunc inspect bool } func openCommandInvocation(parent context.Context, database string) (*commandInvocation, error) { c, verifier, err := parseCommandConfig(os.Getenv(commandConfigEnvironment)) if err != nil { return nil, ErrRefused } return openConfiguredInvocation(parent, database, c, verifier) } func openConfiguredInvocation(parent context.Context, database string, c commandConfig, verifier *Verifier) (*commandInvocation, error) { launch, raw, err := operatorlaunch.ReadOperator(parent, database) if err != nil { return nil, ErrRefused } keep := false defer func() { if !keep { clear(raw) } }() if launch.Account != c.Account || launch.App != c.App || launch.Host != c.Host { return nil, ErrRefused } request, err := verifier.Verify(raw) inspect := false if err != nil { if !verifier.actions["directory.inspect"] { return nil, ErrRefused } if _, err := verifier.verifyInspect(raw, time.Now()); err != nil { return nil, ErrRefused } inspect = true } defer func() { if !keep { clear(request.input) } }() deadline, err := time.Parse(time.RFC3339Nano, launch.ExecutionDeadline) if err != nil || parent.Err() != nil { return nil, ErrRefused } if callerDeadline, ok := parent.Deadline(); ok && callerDeadline.Before(deadline) { deadline = callerDeadline } ctx, cancel := context.WithDeadline(parent, deadline) db, err := newBoundDatabase(launch.DatabasePath, launch.DatabaseDevice, launch.DatabaseInode, deadline) if err != nil { cancel() if err == ErrUnavailable { return nil, ErrUnavailable } return nil, ErrRefused } keep = true return &commandInvocation{db, verifier, request, launch, raw, ctx, cancel, inspect}, nil } func (i *commandInvocation) close() error { if i == nil || i.database == nil { return ErrRefused } defer i.cancel() defer clear(i.raw) defer clear(i.request.input) i.database.connector.mu.Lock() identityErr := i.database.connector.pathMatches() i.database.connector.mu.Unlock() closeErr := i.database.close() i.database = nil if identityErr != nil || closeErr != nil || i.ctx.Err() != nil { return ErrUnavailable } return nil }