package api import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "time" "greq/internal/storage" ) // RunPreScript executes rf.PreScript before sending the request. // The script receives REQUEST_URL, REQUEST_METHOD, REQUEST_BODY as env vars. // Lines of stdout matching "Key: Value" are returned as extra headers to merge // into the request. A non-zero exit is treated as an error (request is aborted). func RunPreScript(script string, rf storage.RequestFile, envs map[string]string) (map[string]string, error) { if strings.TrimSpace(script) == "" { return nil, nil } out, err := runScript(script, map[string]string{ "REQUEST_URL": ResolveVariables(rf.URL, envs), "REQUEST_METHOD": rf.Method, "REQUEST_BODY": rf.Body, }) if err != nil { return nil, fmt.Errorf("pre_script failed: %w\nOutput: %s", err, out) } headers := make(map[string]string) for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if k, v, ok := splitScriptHeader(line); ok { headers[k] = v } } return headers, nil } // RunTestScript executes the test_script field of a request using the Lua runner. // See RunLuaTestScript for available globals and helper functions. func RunTestScript(script string, resp *Response) ([]TestResult, error) { return RunLuaTestScript(script, resp) } // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- func runScript(script string, vars map[string]string) (string, error) { f, err := os.CreateTemp("", "greq-script-*.sh") if err != nil { return "", err } defer os.Remove(f.Name()) if _, err := f.WriteString("#!/bin/sh\n" + script + "\n"); err != nil { f.Close() return "", err } f.Close() if err := os.Chmod(f.Name(), 0o700); err != nil { return "", err } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "sh", f.Name()) // Start with the current process env, then overlay our custom vars // so the script has access to PATH, HOME, etc. cmd.Env = os.Environ() for k, v := range vars { cmd.Env = append(cmd.Env, k+"="+v) } var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out err = cmd.Run() return out.String(), err } func splitScriptHeader(s string) (key, val string, ok bool) { idx := strings.Index(s, ": ") if idx < 1 { return "", "", false } k := strings.TrimSpace(s[:idx]) v := strings.TrimSpace(s[idx+2:]) // Reject lines that look like script output, not headers if strings.ContainsAny(k, " \t=/") { return "", "", false } return k, v, true }