mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
This commit fixes LND RPC authentication by switching from raw gRPC clients to lndclient wrappers that provide proper macaroon authentication. Changes: - Update LNDClients to use lndclient.LightningClient and lndclient.RouterClient instead of raw gRPC clients - Add getRawLightningClient() and getRawRouterClient() helpers that use RawClientWithMacAuth() for authenticated raw client access - Update all 21 LND builtin methods to use the new pattern - Add InMemoryStore implementation for testing - Wire up LND clients in terminal.go after LND connects - Fix integration test function signatures All 7 scripts integration tests now pass: - scripts_basic_crud - scripts_validation - scripts_execution - scripts_kv_store - scripts_lnd_access - scripts_builtins - scripts_kv_builtins
294 lines
7.2 KiB
Go
294 lines
7.2 KiB
Go
package scripting
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"go.starlark.net/starlark"
|
|
"go.starlark.net/syntax"
|
|
)
|
|
|
|
// OutputCallback is called when the script produces output.
|
|
type OutputCallback func(level, message string)
|
|
|
|
// RPCCaller is the interface for making RPC calls from scripts.
|
|
type RPCCaller interface {
|
|
// Call invokes an RPC method with the given request and returns the
|
|
// response.
|
|
Call(ctx context.Context, method string, req interface{}) (interface{}, error)
|
|
}
|
|
|
|
// EngineConfig contains configuration for the Starlark engine.
|
|
type EngineConfig struct {
|
|
// ScriptName is the name of the script being executed.
|
|
ScriptName string
|
|
|
|
// Source is the Starlark source code.
|
|
Source string
|
|
|
|
// Macaroon is the hex-encoded macaroon for RPC authentication.
|
|
Macaroon string
|
|
|
|
// AllowedURLs is the list of URL patterns the script can access.
|
|
AllowedURLs []string
|
|
|
|
// AllowedBuckets is the list of KV buckets the script can access.
|
|
AllowedBuckets []string
|
|
|
|
// Limits are the resource limits for the script.
|
|
Limits ResourceLimits
|
|
|
|
// KVStore is the key-value store for script state.
|
|
KVStore KVStore
|
|
|
|
// RPCCaller is used to make RPC calls.
|
|
RPCCaller RPCCaller
|
|
|
|
// OutputCallback is called when the script produces output.
|
|
OutputCallback OutputCallback
|
|
|
|
// LNDClients provides access to LND RPCs.
|
|
LNDClients *LNDClients
|
|
}
|
|
|
|
// Engine executes Starlark scripts in a sandboxed environment.
|
|
type Engine struct {
|
|
scriptName string
|
|
source string
|
|
macaroon string
|
|
allowedURLs []string
|
|
allowedBuckets []string
|
|
|
|
sandbox *Sandbox
|
|
kvStore KVStore
|
|
rpcCaller RPCCaller
|
|
outputCallback OutputCallback
|
|
lndClients *LNDClients
|
|
|
|
// thread is the Starlark execution thread.
|
|
thread *starlark.Thread
|
|
|
|
// globals holds the script's global variables after execution.
|
|
globals starlark.StringDict
|
|
|
|
// predeclared holds the predeclared functions and modules.
|
|
predeclared starlark.StringDict
|
|
|
|
// mu protects concurrent access to engine state.
|
|
mu sync.Mutex
|
|
|
|
// subscriptionWg tracks active subscription goroutines.
|
|
subscriptionWg sync.WaitGroup
|
|
|
|
// subscriptionHandlers stores handlers for subscription events.
|
|
subscriptionHandlers map[string]starlark.Callable
|
|
}
|
|
|
|
// NewEngine creates a new Starlark execution engine.
|
|
func NewEngine(ctx context.Context, cfg EngineConfig) *Engine {
|
|
sandbox := NewSandbox(ctx, cfg.Limits)
|
|
|
|
e := &Engine{
|
|
scriptName: cfg.ScriptName,
|
|
source: cfg.Source,
|
|
macaroon: cfg.Macaroon,
|
|
allowedURLs: cfg.AllowedURLs,
|
|
allowedBuckets: cfg.AllowedBuckets,
|
|
sandbox: sandbox,
|
|
kvStore: cfg.KVStore,
|
|
rpcCaller: cfg.RPCCaller,
|
|
outputCallback: cfg.OutputCallback,
|
|
lndClients: cfg.LNDClients,
|
|
subscriptionHandlers: make(map[string]starlark.Callable),
|
|
}
|
|
|
|
// Build predeclared functions.
|
|
e.predeclared = make(starlark.StringDict)
|
|
e.registerStandardBuiltins(e.predeclared)
|
|
e.registerHTTPBuiltins(e.predeclared)
|
|
e.registerKVBuiltins(e.predeclared)
|
|
e.registerLNDBuiltins(e.predeclared, cfg.LNDClients)
|
|
|
|
return e
|
|
}
|
|
|
|
// Execute runs the script and calls main() with optional arguments.
|
|
func (e *Engine) Execute(args map[string]interface{}) (*ExecutionResult, error) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
// Create the execution thread.
|
|
e.thread = &starlark.Thread{
|
|
Name: fmt.Sprintf("script:%s", e.scriptName),
|
|
Print: func(thread *starlark.Thread, msg string) {
|
|
log.Infof("[script:%s] %s", e.scriptName, msg)
|
|
if e.outputCallback != nil {
|
|
e.outputCallback("print", msg)
|
|
}
|
|
},
|
|
}
|
|
|
|
// Set up cancellation check.
|
|
e.thread.SetLocal("sandbox", e.sandbox)
|
|
|
|
// Execute the script file to define functions.
|
|
globals, err := starlark.ExecFileOptions(
|
|
&syntax.FileOptions{},
|
|
e.thread,
|
|
e.scriptName+".star",
|
|
e.source,
|
|
e.predeclared,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("script execution failed: %w", err)
|
|
}
|
|
|
|
e.globals = globals
|
|
|
|
// Look for main() function.
|
|
mainFn, ok := globals["main"]
|
|
if !ok {
|
|
// No main function, script just defines variables/functions.
|
|
return &ExecutionResult{
|
|
Success: true,
|
|
Output: "Script executed (no main function)",
|
|
}, nil
|
|
}
|
|
|
|
callable, ok := mainFn.(starlark.Callable)
|
|
if !ok {
|
|
return nil, fmt.Errorf("main is not callable")
|
|
}
|
|
|
|
// Convert args to Starlark kwargs.
|
|
var kwargs []starlark.Tuple
|
|
if args != nil {
|
|
for k, v := range args {
|
|
sv, err := toStarlarkValue(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot convert arg %s: %w", k, err)
|
|
}
|
|
kwargs = append(kwargs, starlark.Tuple{
|
|
starlark.String(k), sv,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Call main().
|
|
result, err := starlark.Call(e.thread, callable, nil, kwargs)
|
|
if err != nil {
|
|
if evalErr, ok := err.(*starlark.EvalError); ok {
|
|
return &ExecutionResult{
|
|
Success: false,
|
|
Error: evalErr.Error(),
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("main() failed: %w", err)
|
|
}
|
|
|
|
// Convert result to JSON.
|
|
var resultJSON string
|
|
if result != starlark.None {
|
|
goResult, err := fromStarlarkValue(result)
|
|
if err != nil {
|
|
resultJSON = result.String()
|
|
} else {
|
|
jsonBytes, err := json.Marshal(goResult)
|
|
if err != nil {
|
|
resultJSON = result.String()
|
|
} else {
|
|
resultJSON = string(jsonBytes)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &ExecutionResult{
|
|
Success: true,
|
|
ResultJSON: resultJSON,
|
|
}, nil
|
|
}
|
|
|
|
// Stop gracefully stops the script execution.
|
|
func (e *Engine) Stop() {
|
|
e.sandbox.Stop()
|
|
|
|
// Wait for subscription goroutines to finish.
|
|
e.subscriptionWg.Wait()
|
|
}
|
|
|
|
// Context returns the sandbox context.
|
|
func (e *Engine) Context() context.Context {
|
|
return e.sandbox.Context()
|
|
}
|
|
|
|
// IsStopped returns whether the engine has been stopped.
|
|
func (e *Engine) IsStopped() bool {
|
|
return e.sandbox.IsStopped()
|
|
}
|
|
|
|
// RegisterSubscriptionHandler registers a Starlark callback for a subscription.
|
|
func (e *Engine) RegisterSubscriptionHandler(name string, handler starlark.Callable) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.subscriptionHandlers[name] = handler
|
|
}
|
|
|
|
// CallHandler safely calls a Starlark handler function.
|
|
func (e *Engine) CallHandler(handler starlark.Callable, args ...starlark.Value) error {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.sandbox.IsStopped() {
|
|
return context.Canceled
|
|
}
|
|
|
|
_, err := starlark.Call(e.thread, handler, args, nil)
|
|
if err != nil {
|
|
if evalErr, ok := err.(*starlark.EvalError); ok {
|
|
log.Errorf("[script:%s] handler error: %s", e.scriptName, evalErr.Error())
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddSubscriptionGoroutine adds a goroutine to the subscription wait group.
|
|
func (e *Engine) AddSubscriptionGoroutine() {
|
|
e.subscriptionWg.Add(1)
|
|
}
|
|
|
|
// DoneSubscriptionGoroutine marks a subscription goroutine as done.
|
|
func (e *Engine) DoneSubscriptionGoroutine() {
|
|
e.subscriptionWg.Done()
|
|
}
|
|
|
|
// GetGlobal returns a global variable from the script.
|
|
func (e *Engine) GetGlobal(name string) (starlark.Value, bool) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if e.globals == nil {
|
|
return nil, false
|
|
}
|
|
|
|
val, ok := e.globals[name]
|
|
return val, ok
|
|
}
|
|
|
|
// ExecutionResult contains the result of script execution.
|
|
type ExecutionResult struct {
|
|
// Success indicates if the script executed successfully.
|
|
Success bool
|
|
|
|
// ResultJSON is the JSON-encoded return value of main().
|
|
ResultJSON string
|
|
|
|
// Output contains any print/log output.
|
|
Output string
|
|
|
|
// Error contains the error message if execution failed.
|
|
Error string
|
|
}
|