all repos — postern

@ 24b6f0d7bd5c4c748592f78a47165959a192fdc2

Modern mail management

clone
git clone https:///postern.git

internal/policy/exec.go (view raw)

  1package policy
  2
  3import (
  4	"bytes"
  5	"context"
  6	"errors"
  7	"fmt"
  8	"os"
  9	"os/exec"
 10	"time"
 11
 12	"go.starlark.net/starlark"
 13)
 14
 15// ExecModule provides the ability to execute external binary commands from Starlark policies.
 16type ExecModule struct{}
 17
 18func (m *ExecModule) String() string       { return "<module exec>" }
 19func (m *ExecModule) Type() string         { return "module" }
 20func (m *ExecModule) Freeze()              {}
 21func (m *ExecModule) Truth() starlark.Bool { return true }
 22func (m *ExecModule) Hash() (uint32, error) {
 23	return 0, errors.New("unhashable")
 24}
 25
 26func (m *ExecModule) Attr(name string) (starlark.Value, error) {
 27	switch name {
 28	case "pipe_to_process":
 29		return starlark.NewBuiltin("pipe_to_process", m.pipeToProcess), nil
 30	}
 31	return nil, nil
 32}
 33
 34func (m *ExecModule) AttrNames() []string {
 35	return []string{"pipe_to_process"}
 36}
 37
 38// pipeToProcess executes an external binary, piping input_data to stdin,
 39// and returns an ExecResult with code, stdout, stderr, and timed_out fields.
 40func (m *ExecModule) pipeToProcess(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
 41	var command string
 42	var inputData starlark.Value = starlark.None
 43	var argList starlark.Value = starlark.None
 44	var timeoutVal starlark.Value = starlark.None
 45	var envDict starlark.Value = starlark.None
 46
 47	if err := starlark.UnpackArgs(b.Name(), args, kwargs,
 48		"command", &command,
 49		"input_data?", &inputData,
 50		"args?", &argList,
 51		"timeout?", &timeoutVal,
 52		"environment?", &envDict,
 53	); err != nil {
 54		return nil, err
 55	}
 56
 57	var inputBytes []byte
 58	switch v := inputData.(type) {
 59	case starlark.String:
 60		inputBytes = []byte(v)
 61	case starlark.Bytes:
 62		inputBytes = []byte(v)
 63	case starlark.NoneType:
 64		inputBytes = nil
 65	default:
 66		return nil, fmt.Errorf("input_data must be a string or bytes, got %s", inputData.Type())
 67	}
 68
 69	var goArgs []string
 70	if argList != starlark.None {
 71		lst, ok := argList.(*starlark.List)
 72		if !ok {
 73			return nil, fmt.Errorf("args must be a list, got %s", argList.Type())
 74		}
 75		for it := range lst.Elements() {
 76			s, ok := it.(starlark.String)
 77			if !ok {
 78				return nil, fmt.Errorf("args must be a list of strings, got %s", it.Type())
 79			}
 80			goArgs = append(goArgs, string(s))
 81		}
 82	}
 83
 84	var timeoutDur = 30 * time.Second
 85	if timeoutVal != starlark.None {
 86		switch v := timeoutVal.(type) {
 87		case starlark.Int:
 88			if i, ok := v.Int64(); ok {
 89				timeoutDur = time.Duration(i) * time.Second
 90			} else {
 91				return nil, errors.New("timeout must be a valid integer")
 92			}
 93		case starlark.Float:
 94			f, _ := starlark.AsFloat(v)
 95			timeoutDur = time.Duration(f) * time.Second
 96		default:
 97			return nil, fmt.Errorf("timeout must be a number, got %s", timeoutVal.Type())
 98		}
 99	}
100
101	hasPath := false
102	var env []string
103	if envDict != starlark.None {
104		dict, ok := envDict.(*starlark.Dict)
105		if !ok {
106			return nil, fmt.Errorf("environment must be a dict, got %s", envDict.Type())
107		}
108		for _, item := range dict.Items() {
109			key, ok := item[0].(starlark.String)
110			if !ok {
111				return nil, fmt.Errorf("environment key must be a string, got %s", item[0].Type())
112			}
113			val, ok := item[1].(starlark.String)
114			if !ok {
115				return nil, fmt.Errorf("environment value must be a string, got %s", item[1].Type())
116			}
117			env = append(env, string(key)+"="+string(val))
118			if key == "PATH" && len(val) > 0 {
119				hasPath = true
120			}
121		}
122	}
123
124	if !hasPath {
125		if path := os.Getenv("PATH"); path != "" {
126			env = append(env, "PATH="+path)
127		}
128	}
129
130	ctx, ok := thread.Local("context").(context.Context)
131	if !ok || ctx == nil {
132		ctx = context.Background()
133	}
134	ctx, cancel := context.WithTimeout(ctx, timeoutDur)
135	defer cancel()
136
137	cmd := exec.CommandContext(ctx, command, goArgs...)
138	cmd.Env = env
139	cmd.Stdin = bytes.NewReader(inputBytes)
140
141	var stdout, stderr bytes.Buffer
142	cmd.Stdout = &stdout
143	cmd.Stderr = &stderr
144
145	runErr := cmd.Run()
146
147	timedOut := errors.Is(ctx.Err(), context.DeadlineExceeded)
148
149	code := 0
150	if runErr != nil {
151		if timedOut {
152			code = -1
153		} else if exitErr, ok := errors.AsType[*exec.ExitError](runErr); ok {
154			code = exitErr.ExitCode()
155		} else {
156			return nil, fmt.Errorf("exec failed: %w", runErr)
157		}
158	}
159
160	return &ExecResult{
161		code:     code,
162		stdout:   stdout.String(),
163		stderr:   stderr.String(),
164		timedOut: timedOut,
165	}, nil
166}
167
168// ExecResult represents the result of an external process execution.
169type ExecResult struct {
170	code     int
171	stdout   string
172	stderr   string
173	timedOut bool
174}
175
176func (r *ExecResult) String() string {
177	return fmt.Sprintf("<ExecResult code=%d>", r.code)
178}
179func (r *ExecResult) Type() string { return "ExecResult" }
180func (r *ExecResult) Freeze()      {}
181func (r *ExecResult) Truth() starlark.Bool {
182	return starlark.Bool(r.code == 0 && !r.timedOut)
183}
184func (r *ExecResult) Hash() (uint32, error) {
185	return 0, errors.New("unhashable")
186}
187
188func (r *ExecResult) Attr(name string) (starlark.Value, error) {
189	switch name {
190	case "code":
191		return starlark.MakeInt(r.code), nil
192	case "stdout":
193		return starlark.String(r.stdout), nil
194	case "stderr":
195		return starlark.String(r.stderr), nil
196	case "timed_out":
197		return starlark.Bool(r.timedOut), nil
198	}
199	return nil, nil
200}
201
202func (r *ExecResult) AttrNames() []string {
203	return []string{"code", "stdout", "stderr", "timed_out"}
204}