2026-04-07 23:22:12 -07:00
|
|
|
package track4
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type stubRoleManager struct {
|
|
|
|
|
allowed bool
|
|
|
|
|
gotIP string
|
|
|
|
|
logs int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *stubRoleManager) IsIPWhitelisted(_ context.Context, _ string, ipAddress string) (bool, error) {
|
|
|
|
|
s.gotIP = ipAddress
|
|
|
|
|
return s.allowed, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *stubRoleManager) LogOperatorEvent(_ context.Context, _ string, _ *int, _ string, _ string, _ string, _ map[string]interface{}, _ string, _ string) error {
|
|
|
|
|
s.logs++
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleRunScriptUsesForwardedClientIPAndRunsAllowlistedScript(t *testing.T) {
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
scriptPath := filepath.Join(root, "echo.sh")
|
|
|
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte("#!/usr/bin/env bash\necho hello \"$1\"\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "echo.sh")
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPT_TIMEOUT_SEC", "30")
|
|
|
|
|
t.Setenv("TRUST_PROXY_CIDRS", "10.0.0.0/8")
|
|
|
|
|
|
|
|
|
|
roleMgr := &stubRoleManager{allowed: true}
|
|
|
|
|
s := &Server{roleMgr: roleMgr, chainID: 138}
|
|
|
|
|
|
|
|
|
|
reqBody := []byte(`{"script":"echo.sh","args":["world"]}`)
|
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader(reqBody))
|
|
|
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
|
|
|
req.RemoteAddr = "10.0.0.10:8080"
|
|
|
|
|
req.Header.Set("X-Forwarded-For", "203.0.113.9, 10.0.0.10")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
|
require.Equal(t, "203.0.113.9", roleMgr.gotIP)
|
|
|
|
|
require.Equal(t, 2, roleMgr.logs)
|
|
|
|
|
|
|
|
|
|
var out struct {
|
|
|
|
|
Data map[string]any `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
|
|
|
|
|
require.Equal(t, "echo.sh", out.Data["script"])
|
|
|
|
|
require.Equal(t, float64(0), out.Data["exit_code"])
|
|
|
|
|
require.Equal(t, "hello world", out.Data["stdout"])
|
|
|
|
|
require.Equal(t, false, out.Data["timed_out"])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleRunScriptRejectsNonAllowlistedScript(t *testing.T) {
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "allowed.sh"), []byte("#!/usr/bin/env bash\necho ok\n"), 0o644))
|
|
|
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "blocked.sh"), []byte("#!/usr/bin/env bash\necho blocked\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "allowed.sh")
|
|
|
|
|
|
|
|
|
|
s := &Server{roleMgr: &stubRoleManager{allowed: true}, chainID: 138}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader([]byte(`{"script":"blocked.sh"}`)))
|
|
|
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
|
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
|
|
|
require.Contains(t, w.Body.String(), "script not in OPERATOR_SCRIPT_ALLOWLIST")
|
|
|
|
|
}
|
2026-04-10 12:52:17 -07:00
|
|
|
|
|
|
|
|
func TestHandleRunScriptRejectsFilenameCollisionOutsideAllowlistedPath(t *testing.T) {
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "safe"), 0o755))
|
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, "unsafe"), 0o755))
|
|
|
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "safe", "backup.sh"), []byte("#!/usr/bin/env bash\necho safe\n"), 0o644))
|
|
|
|
|
require.NoError(t, os.WriteFile(filepath.Join(root, "unsafe", "backup.sh"), []byte("#!/usr/bin/env bash\necho unsafe\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "safe/backup.sh")
|
|
|
|
|
|
|
|
|
|
s := &Server{roleMgr: &stubRoleManager{allowed: true}, chainID: 138}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader([]byte(`{"script":"unsafe/backup.sh"}`)))
|
|
|
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
|
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
|
|
|
require.Contains(t, w.Body.String(), "script not in OPERATOR_SCRIPT_ALLOWLIST")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHandleRunScriptTruncatesLargeOutput(t *testing.T) {
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
scriptPath := filepath.Join(root, "large.sh")
|
|
|
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte("#!/usr/bin/env bash\npython3 - <<'PY'\nprint('x' * 70000)\nPY\n"), 0o644))
|
|
|
|
|
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPTS_ROOT", root)
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPT_ALLOWLIST", "large.sh")
|
|
|
|
|
t.Setenv("OPERATOR_SCRIPT_TIMEOUT_SEC", "30")
|
|
|
|
|
|
|
|
|
|
s := &Server{roleMgr: &stubRoleManager{allowed: true}, chainID: 138}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/track4/operator/run-script", bytes.NewReader([]byte(`{"script":"large.sh"}`)))
|
|
|
|
|
req = req.WithContext(context.WithValue(req.Context(), "user_address", "0x4A666F96fC8764181194447A7dFdb7d471b301C8"))
|
|
|
|
|
req.RemoteAddr = "127.0.0.1:9999"
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
s.HandleRunScript(w, req)
|
|
|
|
|
|
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
|
|
|
|
|
|
var out struct {
|
|
|
|
|
Data struct {
|
|
|
|
|
ExitCode float64 `json:"exit_code"`
|
|
|
|
|
Stdout string `json:"stdout"`
|
|
|
|
|
StdoutTruncated bool `json:"stdout_truncated"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
}
|
|
|
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
|
|
|
|
|
require.Equal(t, float64(0), out.Data.ExitCode)
|
|
|
|
|
require.True(t, out.Data.StdoutTruncated)
|
|
|
|
|
require.Contains(t, out.Data.Stdout, "[truncated after")
|
|
|
|
|
require.LessOrEqual(t, len(out.Data.Stdout), maxOperatorScriptOutputBytes+64)
|
|
|
|
|
}
|