//go:build linux package operator import ( "bytes" "context" "database/sql" "errors" "golang.org/x/sys/unix" "os" "time" ) type CommandExecutor interface { Execute(context.Context, []byte) ([]byte, error) } type CommandMaterial struct { Role, Origin string Sealer Sealer MAC []byte OtherKeys [][]byte CheckSchema func(context.Context, *sql.Conn) error Build func(*Engine) (CommandExecutor, error) } // RunCommand verifies configuration, private launch transport and signature // before exposing the bound database to the role executor. func RunCommand(ctx context.Context, database string, material CommandMaterial) (result *CommandResult, err error) { var out []byte if ctx == nil || material.CheckSchema == nil || material.Build == nil || material.Sealer == nil || len(material.MAC) != 32 || !commandOutputPipe() { return nil, ErrRefused } cfg, verifier, err := parseCommandConfig(os.Getenv(commandConfigEnvironment)) if err != nil || material.Role != cfg.Role || material.Origin != cfg.Origin { return nil, ErrRefused } for _, key := range material.OtherKeys { if bytes.Equal(key, verifier.key) { return nil, ErrRefused } } invocation, err := openConfiguredInvocation(ctx, database, cfg, verifier) if err != nil { return nil, err } defer func() { if closeErr := invocation.close(); closeErr != nil { err = ErrUnavailable } if err != nil { clear(out) out = nil result = nil } }() engine, err := NewEngine(invocation.database.db, verifier, material.Sealer, material.MAC, time.Now) if err != nil { return nil, err } engine.checkSchema = material.CheckSchema executor, err := material.Build(engine) if err != nil || executor == nil { return nil, ErrRefused } if invocation.inspect { directory, ok := executor.(*Directory) if !ok { return nil, ErrRefused } out, err = directory.Inspect(invocation.ctx, invocation.raw) } else { out, err = executor.Execute(invocation.ctx, invocation.raw) } if err != nil { return nil, err } if len(out) == 0 || len(out) > InputLimit { return nil, ErrUnavailable } deadline, _ := invocation.ctx.Deadline() return &CommandResult{out, deadline}, nil } type CommandResult struct { raw []byte deadline time.Time } func (r *CommandResult) Write(ctx context.Context) error { if r == nil { return ErrRefused } defer func() { clear(r.raw); r.raw = nil }() if ctx == nil || len(r.raw) == 0 || !time.Now().Before(r.deadline) { return ErrUnavailable } if !commandOutputPipe() { return ErrUnavailable } if unix.SetNonblock(1, true) != nil { return ErrUnavailable } written := 0 for written < len(r.raw) { if ctx.Err() != nil || !time.Now().Before(r.deadline) { return ErrUnavailable } n, err := unix.Write(1, r.raw[written:]) if n > 0 { written += n } if errors.Is(err, unix.EINTR) { continue } if errors.Is(err, unix.EAGAIN) { timer := time.NewTimer(time.Millisecond) select { case <-ctx.Done(): timer.Stop() return ErrUnavailable case <-timer.C: } continue } if err != nil || n == 0 { return ErrUnavailable } } if unix.Close(1) != nil || ctx.Err() != nil || !time.Now().Before(r.deadline) { return ErrUnavailable } return nil } func commandOutputPipe() bool { var st unix.Stat_t flags, err := unix.FcntlInt(1, unix.F_GETFL, 0) return err == nil && flags&unix.O_ACCMODE == unix.O_WRONLY && unix.Fstat(1, &st) == nil && st.Mode&unix.S_IFMT == unix.S_IFIFO }