diff --git a/pkg/openrpcmanager/client/client.go b/pkg/openrpcmanager/client/client.go deleted file mode 100644 index 8f239d8..0000000 --- a/pkg/openrpcmanager/client/client.go +++ /dev/null @@ -1,209 +0,0 @@ -package client - -import ( - "encoding/json" - "errors" - "fmt" - "net" - - "github.com/freeflowuniverse/heroagent/pkg/openrpcmanager" -) - -// Common errors -var ( - ErrConnectionFailed = errors.New("failed to connect to OpenRPC server") - ErrRequestFailed = errors.New("failed to send request to OpenRPC server") - ErrResponseFailed = errors.New("failed to read response from OpenRPC server") - ErrUnmarshalFailed = errors.New("failed to unmarshal response") - ErrUnexpectedResponse = errors.New("unexpected response format") - ErrRPCError = errors.New("RPC error") - ErrAuthenticationFailed = errors.New("authentication failed") -) - -// RPCRequest represents an outgoing RPC request -type RPCRequest struct { - Method string `json:"method"` - Params json.RawMessage `json:"params"` - ID int `json:"id"` - Secret string `json:"secret,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -// RPCResponse represents an incoming RPC response -type RPCResponse struct { - Result interface{} `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` - ID interface{} `json:"id,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -// RPCError represents an RPC error -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -// Error returns a string representation of the RPC error -func (e *RPCError) Error() string { - if e.Data != nil { - return fmt.Sprintf("RPC error %d: %s - %v", e.Code, e.Message, e.Data) - } - return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message) -} - -// IntrospectionResponse represents the response from the rpc.introspect method -type IntrospectionResponse struct { - Logs []openrpcmanager.CallLog `json:"logs"` - Total int `json:"total"` - Filtered int `json:"filtered"` -} - -// Client is the interface that all OpenRPC clients must implement -type Client interface { - // Discover returns the OpenRPC schema - Discover() (openrpcmanager.OpenRPCSchema, error) - - // Introspect returns information about recent RPC calls - Introspect(limit int, method string, status string) (IntrospectionResponse, error) - - // Request sends a request to the OpenRPC server and returns the result - Request(method string, params json.RawMessage, secret string) (interface{}, error) - - // Close closes the client connection - Close() error -} - -// BaseClient provides a base implementation of the Client interface -type BaseClient struct { - socketPath string - secret string - nextID int -} - -// NewClient creates a new OpenRPC client -func NewClient(socketPath, secret string) *BaseClient { - return &BaseClient{ - socketPath: socketPath, - secret: secret, - nextID: 1, - } -} - -// Discover returns the OpenRPC schema -func (c *BaseClient) Discover() (openrpcmanager.OpenRPCSchema, error) { - result, err := c.Request("rpc.discover", json.RawMessage("{}"), "") - if err != nil { - return openrpcmanager.OpenRPCSchema{}, err - } - - // Convert result to schema - resultJSON, err := json.Marshal(result) - if err != nil { - return openrpcmanager.OpenRPCSchema{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err) - } - - var schema openrpcmanager.OpenRPCSchema - if err := json.Unmarshal(resultJSON, &schema); err != nil { - return openrpcmanager.OpenRPCSchema{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err) - } - - return schema, nil -} - -// Introspect returns information about recent RPC calls -func (c *BaseClient) Introspect(limit int, method string, status string) (IntrospectionResponse, error) { - // Create the params object - params := struct { - Limit int `json:"limit,omitempty"` - Method string `json:"method,omitempty"` - Status string `json:"status,omitempty"` - }{ - Limit: limit, - Method: method, - Status: status, - } - - // Marshal the params - paramsJSON, err := json.Marshal(params) - if err != nil { - return IntrospectionResponse{}, fmt.Errorf("failed to marshal introspection params: %v", err) - } - - // Make the request - result, err := c.Request("rpc.introspect", paramsJSON, c.secret) - if err != nil { - return IntrospectionResponse{}, err - } - - // Convert result to introspection response - resultJSON, err := json.Marshal(result) - if err != nil { - return IntrospectionResponse{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err) - } - - var response IntrospectionResponse - if err := json.Unmarshal(resultJSON, &response); err != nil { - return IntrospectionResponse{}, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err) - } - - return response, nil -} - -// Request sends a request to the OpenRPC server and returns the result -func (c *BaseClient) Request(method string, params json.RawMessage, secret string) (interface{}, error) { - // Connect to the Unix socket - conn, err := net.Dial("unix", c.socketPath) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrConnectionFailed, err) - } - defer conn.Close() - - // Create the request - request := RPCRequest{ - Method: method, - Params: params, - ID: c.nextID, - Secret: secret, - JSONRPC: "2.0", - } - c.nextID++ - - // Marshal the request - requestData, err := json.Marshal(request) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %v", err) - } - - // Send the request - _, err = conn.Write(requestData) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrRequestFailed, err) - } - - // Read the response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - return nil, fmt.Errorf("%w: %v", ErrResponseFailed, err) - } - - // Parse the response - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - return nil, fmt.Errorf("%w: %v", ErrUnmarshalFailed, err) - } - - // Check for errors - if response.Error != nil { - return nil, fmt.Errorf("%w: %v", ErrRPCError, response.Error) - } - - return response.Result, nil -} - -// Close closes the client connection -func (c *BaseClient) Close() error { - // Nothing to do for the base client since we create a new connection for each request - return nil -} diff --git a/pkg/openrpcmanager/client/client_test.go b/pkg/openrpcmanager/client/client_test.go deleted file mode 100644 index 600f71c..0000000 --- a/pkg/openrpcmanager/client/client_test.go +++ /dev/null @@ -1,283 +0,0 @@ -package client - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - "time" - - "github.com/freeflowuniverse/heroagent/pkg/openrpcmanager" -) - -// MockClient implements the Client interface for testing -type MockClient struct { - BaseClient -} - -// TestMethod is a test method that returns a greeting -func (c *MockClient) TestMethod(name string) (string, error) { - params := map[string]string{"name": name} - paramsJSON, err := json.Marshal(params) - if err != nil { - return "", err - } - - result, err := c.Request("test.method", paramsJSON, "") - if err != nil { - return "", err - } - - // Convert result to string - greeting, ok := result.(string) - if !ok { - return "", ErrUnexpectedResponse - } - - return greeting, nil -} - -// SecureMethod is a test method that requires authentication -func (c *MockClient) SecureMethod() (map[string]interface{}, error) { - result, err := c.Request("secure.method", json.RawMessage("{}"), c.secret) - if err != nil { - return nil, err - } - - // Convert result to map - data, ok := result.(map[string]interface{}) - if !ok { - return nil, ErrUnexpectedResponse - } - - return data, nil -} - -func TestClient(t *testing.T) { - // Create a temporary socket path - tempDir, err := os.MkdirTemp("", "openrpc-client-test") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - defer os.RemoveAll(tempDir) - - socketPath := filepath.Join(tempDir, "openrpc.sock") - secret := "test-secret" - - // Create test schema and handlers - schema := openrpcmanager.OpenRPCSchema{ - OpenRPC: "1.2.6", - Info: openrpcmanager.InfoObject{ - Title: "Test API", - Version: "1.0.0", - }, - Methods: []openrpcmanager.MethodObject{ - { - Name: "test.method", - Params: []openrpcmanager.ContentDescriptorObject{ - { - Name: "name", - Schema: openrpcmanager.SchemaObject{"type": "string"}, - }, - }, - Result: &openrpcmanager.ContentDescriptorObject{ - Name: "result", - Schema: openrpcmanager.SchemaObject{"type": "string"}, - }, - }, - { - Name: "secure.method", - Params: []openrpcmanager.ContentDescriptorObject{}, - Result: &openrpcmanager.ContentDescriptorObject{ - Name: "result", - Schema: openrpcmanager.SchemaObject{"type": "object"}, - }, - }, - }, - } - - handlers := map[string]openrpcmanager.RPCHandler{ - "test.method": func(params json.RawMessage) (interface{}, error) { - var request struct { - Name string `json:"name"` - } - if err := json.Unmarshal(params, &request); err != nil { - return nil, err - } - return "Hello, " + request.Name + "!", nil - }, - "secure.method": func(params json.RawMessage) (interface{}, error) { - return map[string]interface{}{ - "secure": true, - "data": "sensitive information", - }, nil - }, - } - - // Create and start OpenRPC manager and Unix server - manager, err := openrpcmanager.NewOpenRPCManager(schema, handlers, secret) - if err != nil { - t.Fatalf("Failed to create OpenRPCManager: %v", err) - } - - server, err := openrpcmanager.NewUnixServer(manager, socketPath) - if err != nil { - t.Fatalf("Failed to create UnixServer: %v", err) - } - - if err := server.Start(); err != nil { - t.Fatalf("Failed to start UnixServer: %v", err) - } - defer server.Stop() - - // Wait for server to start - time.Sleep(100 * time.Millisecond) - - // Create client - client := &MockClient{ - BaseClient: BaseClient{ - socketPath: socketPath, - secret: secret, - }, - } - - // Test Discover method - t.Run("Discover", func(t *testing.T) { - schema, err := client.Discover() - if err != nil { - t.Fatalf("Discover failed: %v", err) - } - - if schema.OpenRPC != "1.2.6" { - t.Errorf("Expected OpenRPC version 1.2.6, got: %s", schema.OpenRPC) - } - - if len(schema.Methods) < 2 { - t.Errorf("Expected at least 2 methods, got: %d", len(schema.Methods)) - } - - // Check if our test methods are in the schema - foundTestMethod := false - foundSecureMethod := false - for _, method := range schema.Methods { - if method.Name == "test.method" { - foundTestMethod = true - } - if method.Name == "secure.method" { - foundSecureMethod = true - } - } - - if !foundTestMethod { - t.Error("test.method not found in schema") - } - if !foundSecureMethod { - t.Error("secure.method not found in schema") - } - }) - - // Test TestMethod - t.Run("TestMethod", func(t *testing.T) { - greeting, err := client.TestMethod("World") - if err != nil { - t.Fatalf("TestMethod failed: %v", err) - } - - expected := "Hello, World!" - if greeting != expected { - t.Errorf("Expected greeting %q, got: %q", expected, greeting) - } - }) - - // Test Introspect method - t.Run("Introspect", func(t *testing.T) { - // Make several requests to generate logs - _, err := client.TestMethod("World") - if err != nil { - t.Fatalf("TestMethod failed: %v", err) - } - - _, err = client.SecureMethod() - if err != nil { - t.Fatalf("SecureMethod failed: %v", err) - } - - // Test introspection - response, err := client.Introspect(10, "", "") - if err != nil { - t.Fatalf("Introspect failed: %v", err) - } - - // Verify we have logs - if response.Total < 2 { - t.Errorf("Expected at least 2 logs, got: %d", response.Total) - } - - // Test filtering by method - response, err = client.Introspect(10, "test.method", "") - if err != nil { - t.Fatalf("Introspect with method filter failed: %v", err) - } - - // Verify filtering works - for _, log := range response.Logs { - if log.Method != "test.method" { - t.Errorf("Expected only test.method logs, got: %s", log.Method) - } - } - - // Test filtering by status - response, err = client.Introspect(10, "", "success") - if err != nil { - t.Fatalf("Introspect with status filter failed: %v", err) - } - - // Verify status filtering works - for _, log := range response.Logs { - if log.Status != "success" { - t.Errorf("Expected only success logs, got: %s", log.Status) - } - } - }) - - // Test SecureMethod with valid secret - t.Run("SecureMethod", func(t *testing.T) { - data, err := client.SecureMethod() - if err != nil { - t.Fatalf("SecureMethod failed: %v", err) - } - - secure, ok := data["secure"].(bool) - if !ok || !secure { - t.Errorf("Expected secure to be true, got: %v", data["secure"]) - } - - sensitiveData, ok := data["data"].(string) - if !ok || sensitiveData != "sensitive information" { - t.Errorf("Expected data to be 'sensitive information', got: %v", data["data"]) - } - }) - - // Test SecureMethod with invalid secret - t.Run("SecureMethod with invalid secret", func(t *testing.T) { - invalidClient := &MockClient{ - BaseClient: BaseClient{ - socketPath: socketPath, - secret: "wrong-secret", - }, - } - - _, err := invalidClient.SecureMethod() - if err == nil { - t.Error("Expected error for invalid secret, but got nil") - } - }) - - // Test non-existent method - t.Run("Non-existent method", func(t *testing.T) { - _, err := client.Request("non.existent", json.RawMessage("{}"), "") - if err == nil { - t.Error("Expected error for non-existent method, but got nil") - } - }) -} diff --git a/pkg/openrpcmanager/cmd/server/main.go b/pkg/openrpcmanager/cmd/server/main.go deleted file mode 100644 index 76b397a..0000000 --- a/pkg/openrpcmanager/cmd/server/main.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "log" - "os" - "os/signal" - "syscall" - - "github.com/freeflowuniverse/heroagent/pkg/openrpcmanager" -) - -func main() { - // Parse command line arguments - socketPath := flag.String("socket", "/tmp/openrpc.sock", "Path to the Unix socket") - secret := flag.String("secret", "test-secret", "Secret for authenticated methods") - flag.Parse() - - // Create a simple OpenRPC schema - schema := openrpcmanager.OpenRPCSchema{ - OpenRPC: "1.2.6", - Info: openrpcmanager.InfoObject{ - Title: "Hero Launcher API", - Version: "1.0.0", - }, - Methods: []openrpcmanager.MethodObject{ - { - Name: "echo", - Params: []openrpcmanager.ContentDescriptorObject{ - { - Name: "message", - Schema: openrpcmanager.SchemaObject{"type": "object"}, - }, - }, - Result: &openrpcmanager.ContentDescriptorObject{ - Name: "result", - Schema: openrpcmanager.SchemaObject{"type": "object"}, - }, - }, - { - Name: "ping", - Params: []openrpcmanager.ContentDescriptorObject{}, - Result: &openrpcmanager.ContentDescriptorObject{ - Name: "result", - Schema: openrpcmanager.SchemaObject{"type": "string"}, - }, - }, - { - Name: "secure.info", - Params: []openrpcmanager.ContentDescriptorObject{}, - Result: &openrpcmanager.ContentDescriptorObject{ - Name: "result", - Schema: openrpcmanager.SchemaObject{"type": "object"}, - }, - }, - }, - } - - // Create handlers - handlers := map[string]openrpcmanager.RPCHandler{ - "echo": func(params json.RawMessage) (interface{}, error) { - var data interface{} - if err := json.Unmarshal(params, &data); err != nil { - return nil, err - } - return data, nil - }, - "ping": func(params json.RawMessage) (interface{}, error) { - return "pong", nil - }, - "secure.info": func(params json.RawMessage) (interface{}, error) { - return map[string]interface{}{ - "server": "Hero Launcher", - "version": "1.0.0", - "status": "running", - }, nil - }, - } - - // Create OpenRPC manager - manager, err := openrpcmanager.NewOpenRPCManager(schema, handlers, *secret) - if err != nil { - log.Fatalf("Failed to create OpenRPC manager: %v", err) - } - - // Create Unix server - server, err := openrpcmanager.NewUnixServer(manager, *socketPath) - if err != nil { - log.Fatalf("Failed to create Unix server: %v", err) - } - - // Start the server - if err := server.Start(); err != nil { - log.Fatalf("Failed to start Unix server: %v", err) - } - defer server.Stop() - - fmt.Printf("OpenRPC server started on Unix socket: %s\n", *socketPath) - fmt.Println("Available methods:") - for _, method := range manager.ListMethods() { - fmt.Printf(" - %s\n", method) - } - fmt.Println("\nPress Ctrl+C to stop the server") - - // Wait for interrupt signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh - - fmt.Println("\nShutting down...") -} diff --git a/pkg/openrpcmanager/examples/unixclient/main.go b/pkg/openrpcmanager/examples/unixclient/main.go deleted file mode 100644 index 81e240d..0000000 --- a/pkg/openrpcmanager/examples/unixclient/main.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "encoding/json" - "flag" - "fmt" - "net" - "os" -) - -// RPCRequest represents an outgoing RPC request -type RPCRequest struct { - Method string `json:"method"` - Params json.RawMessage `json:"params"` - ID interface{} `json:"id,omitempty"` - Secret string `json:"secret,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -// RPCResponse represents an incoming RPC response -type RPCResponse struct { - Result interface{} `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` - ID interface{} `json:"id,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -// RPCError represents an RPC error -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -func main() { - // Parse command line arguments - socketPath := flag.String("socket", "/tmp/openrpc.sock", "Path to the Unix socket") - method := flag.String("method", "rpc.discover", "RPC method to call") - params := flag.String("params", "{}", "JSON parameters for the method") - secret := flag.String("secret", "", "Secret for authenticated methods") - flag.Parse() - - // Connect to the Unix socket - conn, err := net.Dial("unix", *socketPath) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to connect to Unix socket: %v\n", err) - os.Exit(1) - } - defer conn.Close() - - // Create the request - var paramsJSON json.RawMessage - if err := json.Unmarshal([]byte(*params), ¶msJSON); err != nil { - fmt.Fprintf(os.Stderr, "Invalid JSON parameters: %v\n", err) - os.Exit(1) - } - - request := RPCRequest{ - Method: *method, - Params: paramsJSON, - ID: 1, - Secret: *secret, - JSONRPC: "2.0", - } - - // Marshal the request - requestData, err := json.Marshal(request) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to marshal request: %v\n", err) - os.Exit(1) - } - - // Send the request - _, err = conn.Write(requestData) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to send request: %v\n", err) - os.Exit(1) - } - - // Read the response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read response: %v\n", err) - os.Exit(1) - } - - // Parse the response - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - fmt.Fprintf(os.Stderr, "Failed to unmarshal response: %v\n", err) - os.Exit(1) - } - - // Check for errors - if response.Error != nil { - fmt.Fprintf(os.Stderr, "Error: %s (code: %d)\n", response.Error.Message, response.Error.Code) - if response.Error.Data != nil { - fmt.Fprintf(os.Stderr, "Error data: %v\n", response.Error.Data) - } - os.Exit(1) - } - - // Print the result - resultJSON, err := json.MarshalIndent(response.Result, "", " ") - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to marshal result: %v\n", err) - os.Exit(1) - } - - fmt.Println(string(resultJSON)) -} diff --git a/pkg/openrpcmanager/openrpcmanager.go b/pkg/openrpcmanager/openrpcmanager.go deleted file mode 100644 index 815d166..0000000 --- a/pkg/openrpcmanager/openrpcmanager.go +++ /dev/null @@ -1,416 +0,0 @@ -// Package openrpcmanager provides functionality for managing and handling OpenRPC method calls. -package openrpcmanager - -import ( - "encoding/json" - "fmt" - "sync" - "time" -) - -// RPCHandler is a function that handles an OpenRPC method call -type RPCHandler func(params json.RawMessage) (interface{}, error) - -// CallLog represents a log entry for an RPC method call -type CallLog struct { - Timestamp time.Time `json:"timestamp"` // When the call was made - Method string `json:"method"` // Method that was called - Params interface{} `json:"params"` // Parameters passed to the method (may be redacted for security) - Duration time.Duration `json:"duration"` // How long the call took to execute - Status string `json:"status"` // Success or error - ErrorMsg string `json:"error,omitempty"` // Error message if status is error - Authenticated bool `json:"authenticated"` // Whether the call was authenticated -} - -// OpenRPCManager manages OpenRPC method handlers and processes requests -type OpenRPCManager struct { - handlers map[string]RPCHandler - schema OpenRPCSchema - mutex sync.RWMutex - secret string - - // Call logging - callLogs []CallLog - callLogsMutex sync.RWMutex - maxCallLogs int // Maximum number of call logs to keep -} - -// NewOpenRPCManager creates a new OpenRPC manager with the given schema and handlers -func NewOpenRPCManager(schema OpenRPCSchema, handlers map[string]RPCHandler, secret string) (*OpenRPCManager, error) { - manager := &OpenRPCManager{ - handlers: make(map[string]RPCHandler), - schema: schema, - secret: secret, - callLogs: make([]CallLog, 0, 100), - maxCallLogs: 1000, // Default to keeping the last 1000 calls - } - - // Validate that all methods in the schema have corresponding handlers - for _, method := range schema.Methods { - handler, exists := handlers[method.Name] - if !exists { - return nil, fmt.Errorf("missing handler for method '%s' defined in schema", method.Name) - } - manager.handlers[method.Name] = handler - } - - // Check for handlers that don't have a corresponding method in the schema - for name := range handlers { - found := false - for _, method := range schema.Methods { - if method.Name == name { - found = true - break - } - } - if !found { - return nil, fmt.Errorf("handler '%s' has no corresponding method in schema", name) - } - } - - // Add the discovery method - manager.handlers["rpc.discover"] = manager.handleDiscovery - - // Add the introspection method - manager.handlers["rpc.introspect"] = manager.handleIntrospection - - return manager, nil -} - -// handleDiscovery implements the OpenRPC service discovery method -func (m *OpenRPCManager) handleDiscovery(params json.RawMessage) (interface{}, error) { - return m.schema, nil -} - -// handleIntrospection implements the OpenRPC service introspection method -// It returns information about recent RPC calls for monitoring and debugging -func (m *OpenRPCManager) handleIntrospection(params json.RawMessage) (interface{}, error) { - m.callLogsMutex.RLock() - defer m.callLogsMutex.RUnlock() - - // Parse parameters to see if we need to filter or limit results - var requestParams struct { - Limit int `json:"limit,omitempty"` - Method string `json:"method,omitempty"` - Status string `json:"status,omitempty"` - } - - // Default limit to 100 if not specified - requestParams.Limit = 100 - - // Try to parse parameters, but don't fail if they're invalid - if len(params) > 0 { - _ = json.Unmarshal(params, &requestParams) - } - - // Apply limit - if requestParams.Limit <= 0 || requestParams.Limit > m.maxCallLogs { - requestParams.Limit = 100 - } - - // Create a copy of the logs to avoid holding the lock while filtering - allLogs := make([]CallLog, len(m.callLogs)) - copy(allLogs, m.callLogs) - - // Filter logs based on parameters - filteredLogs := []CallLog{} - for i := len(allLogs) - 1; i >= 0 && len(filteredLogs) < requestParams.Limit; i-- { - log := allLogs[i] - - // Apply method filter if specified - if requestParams.Method != "" && log.Method != requestParams.Method { - continue - } - - // Apply status filter if specified - if requestParams.Status != "" && log.Status != requestParams.Status { - continue - } - - filteredLogs = append(filteredLogs, log) - } - - // Create response - response := struct { - Logs []CallLog `json:"logs"` - Total int `json:"total"` - Filtered int `json:"filtered"` - }{ - Logs: filteredLogs, - Total: len(allLogs), - Filtered: len(filteredLogs), - } - - return response, nil -} - -// RegisterHandler is deprecated. Use NewOpenRPCManager with a complete set of handlers instead. -// This method is kept for backward compatibility but will return an error for methods not in the schema. -func (m *OpenRPCManager) RegisterHandler(method string, handler RPCHandler) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - // Check if handler already exists - if _, exists := m.handlers[method]; exists { - return fmt.Errorf("handler for method '%s' already registered", method) - } - - // Check if method exists in schema - found := false - for _, schemaMethod := range m.schema.Methods { - if schemaMethod.Name == method { - found = true - break - } - } - - if !found && method != "rpc.discover" { - return fmt.Errorf("method '%s' not defined in schema", method) - } - - m.handlers[method] = handler - return nil -} - -// UnregisterHandler removes a handler for the specified method -// Note: This will make the service non-compliant with its schema -func (m *OpenRPCManager) UnregisterHandler(method string) error { - m.mutex.Lock() - defer m.mutex.Unlock() - - // Check if handler exists - if _, exists := m.handlers[method]; !exists { - return fmt.Errorf("handler for method '%s' not found", method) - } - - // Don't allow unregistering the discovery method - if method == "rpc.discover" { - return fmt.Errorf("cannot unregister the discovery method 'rpc.discover'") - } - - delete(m.handlers, method) - return nil -} - -// HandleRequest processes an RPC request for the specified method -func (m *OpenRPCManager) HandleRequest(method string, params json.RawMessage) (interface{}, error) { - // Start timing the request - startTime := time.Now() - - // Create a call log entry - callLog := CallLog{ - Timestamp: startTime, - Method: method, - Authenticated: false, - } - - // Parse params for logging, but don't fail if we can't - var parsedParams interface{} - if len(params) > 0 { - if err := json.Unmarshal(params, &parsedParams); err == nil { - callLog.Params = parsedParams - } else { - // If we can't parse the params, just store them as a string - callLog.Params = string(params) - } - } - - // Find the handler - m.mutex.RLock() - handler, exists := m.handlers[method] - m.mutex.RUnlock() - - if !exists { - // Log the error - callLog.Status = "error" - callLog.ErrorMsg = fmt.Sprintf("method '%s' not found", method) - callLog.Duration = time.Since(startTime) - - // Add to call logs - m.logCall(callLog) - - return nil, fmt.Errorf("method '%s' not found", method) - } - - // Execute the handler - result, err := handler(params) - - // Complete the call log - callLog.Duration = time.Since(startTime) - if err != nil { - callLog.Status = "error" - callLog.ErrorMsg = err.Error() - } else { - callLog.Status = "success" - } - - // Add to call logs - m.logCall(callLog) - - return result, err -} - -// logCall adds a call log entry to the call logs, maintaining the maximum size -func (m *OpenRPCManager) logCall(log CallLog) { - m.callLogsMutex.Lock() - defer m.callLogsMutex.Unlock() - - // Add the log to the call logs - m.callLogs = append(m.callLogs, log) - - // Trim the call logs if they exceed the maximum size - if len(m.callLogs) > m.maxCallLogs { - m.callLogs = m.callLogs[len(m.callLogs)-m.maxCallLogs:] - } -} - -// HandleRequestWithAuthentication processes an authenticated RPC request -func (m *OpenRPCManager) HandleRequestWithAuthentication(method string, params json.RawMessage, secret string) (interface{}, error) { - // Start timing the request - startTime := time.Now() - - // Create a call log entry - callLog := CallLog{ - Timestamp: startTime, - Method: method, - Authenticated: true, - } - - // Parse params for logging, but don't fail if we can't - var parsedParams interface{} - if len(params) > 0 { - if err := json.Unmarshal(params, &parsedParams); err == nil { - callLog.Params = parsedParams - } else { - // If we can't parse the params, just store them as a string - callLog.Params = string(params) - } - } - - // Verify the secret - if secret != m.secret { - // Log the authentication failure - callLog.Status = "error" - callLog.ErrorMsg = "authentication failed" - callLog.Duration = time.Since(startTime) - m.logCall(callLog) - - return nil, fmt.Errorf("authentication failed") - } - - // Execute the handler - m.mutex.RLock() - handler, exists := m.handlers[method] - m.mutex.RUnlock() - - if !exists { - // Log the error - callLog.Status = "error" - callLog.ErrorMsg = fmt.Sprintf("method '%s' not found", method) - callLog.Duration = time.Since(startTime) - m.logCall(callLog) - - return nil, fmt.Errorf("method '%s' not found", method) - } - - // Execute the handler - result, err := handler(params) - - // Complete the call log - callLog.Duration = time.Since(startTime) - if err != nil { - callLog.Status = "error" - callLog.ErrorMsg = err.Error() - } else { - callLog.Status = "success" - } - - // Add to call logs - m.logCall(callLog) - - return result, err -} - -// ListMethods returns a list of all registered method names -func (m *OpenRPCManager) ListMethods() []string { - m.mutex.RLock() - defer m.mutex.RUnlock() - - methods := make([]string, 0, len(m.handlers)) - for method := range m.handlers { - methods = append(methods, method) - } - - return methods -} - -// GetSecret returns the authentication secret -func (m *OpenRPCManager) GetSecret() string { - return m.secret -} - -// GetSchema returns the OpenRPC schema -func (m *OpenRPCManager) GetSchema() OpenRPCSchema { - return m.schema -} - -// NewDefaultOpenRPCManager creates a new OpenRPC manager with a default schema -// This is provided for backward compatibility and testing -func NewDefaultOpenRPCManager(secret string) *OpenRPCManager { - // Create a minimal default schema - defaultSchema := OpenRPCSchema{ - OpenRPC: "1.2.6", - Info: InfoObject{ - Title: "Default OpenRPC Service", - Version: "1.0.0", - }, - Methods: []MethodObject{ - { - Name: "rpc.discover", - Description: "Returns the OpenRPC schema for this service", - Params: []ContentDescriptorObject{}, - Result: &ContentDescriptorObject{ - Name: "schema", - Description: "The OpenRPC schema", - Schema: SchemaObject{"type": "object"}, - }, - }, - { - Name: "rpc.introspect", - Description: "Returns information about recent RPC calls for monitoring and debugging", - Params: []ContentDescriptorObject{ - { - Name: "limit", - Description: "Maximum number of call logs to return", - Required: false, - Schema: SchemaObject{"type": "integer", "default": 100}, - }, - { - Name: "method", - Description: "Filter logs by method name", - Required: false, - Schema: SchemaObject{"type": "string"}, - }, - { - Name: "status", - Description: "Filter logs by status (success or error)", - Required: false, - Schema: SchemaObject{"type": "string", "enum": []interface{}{"success", "error"}}, - }, - }, - Result: &ContentDescriptorObject{ - Name: "introspection", - Description: "Introspection data including call logs", - Schema: SchemaObject{"type": "object"}, - }, - }, - }, - } - - // Create the manager directly without validation since we're starting with empty methods - return &OpenRPCManager{ - handlers: make(map[string]RPCHandler), - schema: defaultSchema, - secret: secret, - } -} diff --git a/pkg/openrpcmanager/openrpcmanager_test.go b/pkg/openrpcmanager/openrpcmanager_test.go deleted file mode 100644 index 3780f01..0000000 --- a/pkg/openrpcmanager/openrpcmanager_test.go +++ /dev/null @@ -1,446 +0,0 @@ -package openrpcmanager - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -// createTestSchema creates a test OpenRPC schema -func createTestSchema() OpenRPCSchema { - return OpenRPCSchema{ - OpenRPC: "1.2.6", - Info: InfoObject{ - Title: "Test API", - Version: "1.0.0", - }, - Methods: []MethodObject{ - { - Name: "echo", - Params: []ContentDescriptorObject{ - { - Name: "message", - Schema: SchemaObject{"type": "object"}, - }, - }, - Result: &ContentDescriptorObject{ - Name: "result", - Schema: SchemaObject{"type": "object"}, - }, - }, - { - Name: "add", - Params: []ContentDescriptorObject{ - { - Name: "numbers", - Schema: SchemaObject{"type": "object"}, - }, - }, - Result: &ContentDescriptorObject{ - Name: "result", - Schema: SchemaObject{"type": "number"}, - }, - }, - { - Name: "secure.method", - Params: []ContentDescriptorObject{}, - Result: &ContentDescriptorObject{ - Name: "result", - Schema: SchemaObject{"type": "string"}, - }, - }, - }, - } -} - -// createTestHandlers creates test handlers for the schema -func createTestHandlers() map[string]RPCHandler { - return map[string]RPCHandler{ - "echo": func(params json.RawMessage) (interface{}, error) { - var data interface{} - if err := json.Unmarshal(params, &data); err != nil { - return nil, err - } - return data, nil - }, - "add": func(params json.RawMessage) (interface{}, error) { - var numbers struct { - A float64 `json:"a"` - B float64 `json:"b"` - } - if err := json.Unmarshal(params, &numbers); err != nil { - return nil, err - } - return numbers.A + numbers.B, nil - }, - "secure.method": func(params json.RawMessage) (interface{}, error) { - return "secure data", nil - }, - } -} - -// TestNewOpenRPCManager tests the creation of a new OpenRPC manager -func TestNewOpenRPCManager(t *testing.T) { - secret := "test-secret" - schema := createTestSchema() - handlers := createTestHandlers() - - manager, err := NewOpenRPCManager(schema, handlers, secret) - if err != nil { - t.Fatalf("Failed to create OpenRPCManager: %v", err) - } - - if manager == nil { - t.Fatal("Manager is nil") - } - - if manager.GetSecret() != secret { - t.Errorf("Secret mismatch. Expected: %s, Got: %s", secret, manager.GetSecret()) - } - - if manager.handlers == nil { - t.Error("handlers map not initialized") - } - - // Test the default manager for backward compatibility - defaultManager := NewDefaultOpenRPCManager(secret) - if defaultManager == nil { - t.Fatal("Default manager is nil") - } - - if defaultManager.GetSecret() != secret { - t.Errorf("Secret mismatch in default manager. Expected: %s, Got: %s", secret, defaultManager.GetSecret()) - } -} - -// TestRegisterHandler tests registering a handler to the OpenRPC manager -func TestRegisterHandler(t *testing.T) { - manager := NewDefaultOpenRPCManager("test-secret") - - // Define a mock handler - mockHandler := func(params json.RawMessage) (interface{}, error) { - return "mock response", nil - } - - // Add a method to the schema - manager.schema.Methods = append(manager.schema.Methods, MethodObject{ - Name: "test.method", - Params: []ContentDescriptorObject{}, - Result: &ContentDescriptorObject{ - Name: "result", - Schema: SchemaObject{"type": "string"}, - }, - }) - - // Register the handler - err := manager.RegisterHandler("test.method", mockHandler) - if err != nil { - t.Fatalf("Failed to register handler: %v", err) - } - - // Check if handler was registered - if _, exists := manager.handlers["test.method"]; !exists { - t.Error("Handler was not registered") - } - - // Try to register the same handler again, should fail - err = manager.RegisterHandler("test.method", mockHandler) - if err == nil { - t.Error("Expected error when registering duplicate handler, but got nil") - } - - // Try to register a handler for a method not in the schema - err = manager.RegisterHandler("not.in.schema", mockHandler) - if err == nil { - t.Error("Expected error when registering handler for method not in schema, but got nil") - } -} - -// TestHandleRequest tests handling an RPC request -func TestHandleRequest(t *testing.T) { - schema := createTestSchema() - handlers := createTestHandlers() - manager, err := NewOpenRPCManager(schema, handlers, "test-secret") - if err != nil { - t.Fatalf("Failed to create OpenRPCManager: %v", err) - } - - // Test the echo handler - testParams := json.RawMessage(`{"message":"hello world"}`) - result, err := manager.HandleRequest("echo", testParams) - if err != nil { - t.Fatalf("Failed to handle request: %v", err) - } - - // Convert result to map for comparison - resultMap, ok := result.(map[string]interface{}) - if !ok { - t.Fatalf("Expected map result, got: %T", result) - } - - if resultMap["message"] != "hello world" { - t.Errorf("Expected 'hello world', got: %v", resultMap["message"]) - } - - // Test the add handler - addParams := json.RawMessage(`{"a":5,"b":7}`) - addResult, err := manager.HandleRequest("add", addParams) - if err != nil { - t.Fatalf("Failed to handle add request: %v", err) - } - - // Check the result type and value - resultValue, ok := addResult.(float64) - if !ok { - t.Fatalf("Expected result type float64, got: %T", addResult) - } - if resultValue != float64(12) { - t.Errorf("Expected 12, got: %v", resultValue) - } - - // Test with non-existent method - _, err = manager.HandleRequest("nonexistent", testParams) - if err == nil { - t.Error("Expected error for non-existent method, but got nil") - } - - // Test the discovery method - discoveryResult, err := manager.HandleRequest("rpc.discover", json.RawMessage(`{}`)) - if err != nil { - t.Fatalf("Failed to handle discovery request: %v", err) - } - - // Verify the discovery result is the schema - discoverySchema, ok := discoveryResult.(OpenRPCSchema) - if !ok { - t.Fatalf("Expected OpenRPCSchema result, got: %T", discoveryResult) - } - - if discoverySchema.OpenRPC != schema.OpenRPC { - t.Errorf("Expected OpenRPC version %s, got: %s", schema.OpenRPC, discoverySchema.OpenRPC) - } - - if len(discoverySchema.Methods) != len(schema.Methods) { - t.Errorf("Expected %d methods, got: %d", len(schema.Methods), len(discoverySchema.Methods)) - } -} - -// TestHandleRequestWithAuthentication tests handling a request with authentication -func TestHandleRequestWithAuthentication(t *testing.T) { - secret := "test-secret" - schema := createTestSchema() - handlers := createTestHandlers() - manager, err := NewOpenRPCManager(schema, handlers, secret) - if err != nil { - t.Fatalf("Failed to create OpenRPCManager: %v", err) - } - - // Test with correct secret - result, err := manager.HandleRequestWithAuthentication("secure.method", json.RawMessage(`{}`), secret) - if err != nil { - t.Fatalf("Failed to handle authenticated request: %v", err) - } - - if result != "secure data" { - t.Errorf("Expected 'secure data', got: %v", result) - } - - // Test with incorrect secret - _, err = manager.HandleRequestWithAuthentication("secure.method", json.RawMessage(`{}`), "wrong-secret") - if err == nil { - t.Error("Expected authentication error, but got nil") - } -} - -// TestUnregisterHandler tests removing a handler from the OpenRPC manager -func TestUnregisterHandler(t *testing.T) { - manager := NewDefaultOpenRPCManager("test-secret") - - // Define a mock handler - mockHandler := func(params json.RawMessage) (interface{}, error) { - return "mock response", nil - } - - // Add a method to the schema - manager.schema.Methods = append(manager.schema.Methods, MethodObject{ - Name: "test.method", - Params: []ContentDescriptorObject{}, - Result: &ContentDescriptorObject{ - Name: "result", - Schema: SchemaObject{"type": "string"}, - }, - }) - - // Register the handler - manager.RegisterHandler("test.method", mockHandler) - - // Unregister the handler - err := manager.UnregisterHandler("test.method") - if err != nil { - t.Fatalf("Failed to unregister handler: %v", err) - } - - // Check if handler was unregistered - if _, exists := manager.handlers["test.method"]; exists { - t.Error("Handler was not unregistered") - } - - // Try to unregister non-existent handler - err = manager.UnregisterHandler("nonexistent") - if err == nil { - t.Error("Expected error when unregistering non-existent handler, but got nil") - } - - // Try to unregister the discovery method - err = manager.UnregisterHandler("rpc.discover") - if err == nil { - t.Error("Expected error when unregistering discovery method, but got nil") - } -} - -// TestIntrospection tests the introspection functionality -func TestIntrospection(t *testing.T) { - // Create a test manager - schema := createTestSchema() - handlers := createTestHandlers() - manager, err := NewOpenRPCManager(schema, handlers, "test-secret") - assert.NoError(t, err) - - // The introspection handler is already registered in NewOpenRPCManager - - // Make some test calls to generate logs - _, err = manager.HandleRequest("echo", json.RawMessage(`{"message":"hello"}`)) - assert.NoError(t, err) - - _, err = manager.HandleRequestWithAuthentication("echo", json.RawMessage(`{"message":"authenticated"}`), "test-secret") - assert.NoError(t, err) - - _, err = manager.HandleRequestWithAuthentication("echo", json.RawMessage(`{"message":"auth-fail"}`), "wrong-secret") - assert.Error(t, err) - - // Wait a moment to ensure timestamps are different - time.Sleep(10 * time.Millisecond) - - // Call the introspection handler - result, err := manager.handleIntrospection(json.RawMessage(`{"limit":10}`)) - assert.NoError(t, err) - - // Verify the result - response, ok := result.(struct { - Logs []CallLog `json:"logs"` - Total int `json:"total"` - Filtered int `json:"filtered"` - }) - assert.True(t, ok) - - // Should have 3 logs (2 successful calls, 1 auth failure) - assert.Equal(t, 3, response.Total) - assert.Equal(t, 3, response.Filtered) - assert.Len(t, response.Logs, 3) - - // Test filtering by method - result, err = manager.handleIntrospection(json.RawMessage(`{"method":"echo"}`)) - assert.NoError(t, err) - response, ok = result.(struct { - Logs []CallLog `json:"logs"` - Total int `json:"total"` - Filtered int `json:"filtered"` - }) - assert.True(t, ok) - assert.Equal(t, 3, response.Total) // Total is still 3 - assert.Equal(t, 3, response.Filtered) // All 3 match the method filter - - // Test filtering by status - result, err = manager.handleIntrospection(json.RawMessage(`{"status":"error"}`)) - assert.NoError(t, err) - response, ok = result.(struct { - Logs []CallLog `json:"logs"` - Total int `json:"total"` - Filtered int `json:"filtered"` - }) - assert.True(t, ok) - assert.Equal(t, 3, response.Total) // Total is still 3 - assert.Equal(t, 1, response.Filtered) // Only 1 error - assert.Len(t, response.Logs, 1) - assert.Equal(t, "error", response.Logs[0].Status) -} - -// TestListMethods tests listing all registered methods -func TestListMethods(t *testing.T) { - schema := createTestSchema() - handlers := createTestHandlers() - manager, err := NewOpenRPCManager(schema, handlers, "test-secret") - if err != nil { - t.Fatalf("Failed to create OpenRPCManager: %v", err) - } - - // List all methods - registeredMethods := manager.ListMethods() - - // Check if all methods plus discovery and introspection methods are listed - expectedCount := len(schema.Methods) + 2 // +2 for rpc.discover and rpc.introspect - if len(registeredMethods) != expectedCount { - t.Errorf("Expected %d methods, got %d", expectedCount, len(registeredMethods)) - } - - // Check if all schema methods are in the list - for _, methodObj := range schema.Methods { - found := false - for _, registeredMethod := range registeredMethods { - if registeredMethod == methodObj.Name { - found = true - break - } - } - if !found { - t.Errorf("Method %s not found in list", methodObj.Name) - } - } - - // Check if discovery method is in the list - found := false - for _, registeredMethod := range registeredMethods { - if registeredMethod == "rpc.discover" { - found = true - break - } - } - if !found { - t.Error("Discovery method 'rpc.discover' not found in list") - } -} - -// TestSchemaValidation tests that the schema validation works correctly -func TestSchemaValidation(t *testing.T) { - secret := "test-secret" - schema := createTestSchema() - - // Test with missing handler - incompleteHandlers := map[string]RPCHandler{ - "echo": func(params json.RawMessage) (interface{}, error) { - return nil, nil - }, - // Missing "add" handler - "secure.method": func(params json.RawMessage) (interface{}, error) { - return nil, nil - }, - } - - _, err := NewOpenRPCManager(schema, incompleteHandlers, secret) - if err == nil { - t.Error("Expected error when missing handler for schema method, but got nil") - } - - // Test with extra handler not in schema - extraHandlers := createTestHandlers() - extraHandlers["not.in.schema"] = func(params json.RawMessage) (interface{}, error) { - return nil, nil - } - - _, err = NewOpenRPCManager(schema, extraHandlers, secret) - if err == nil { - t.Error("Expected error when handler has no corresponding method in schema, but got nil") - } -} diff --git a/pkg/openrpcmanager/schema.go b/pkg/openrpcmanager/schema.go deleted file mode 100644 index ce714fc..0000000 --- a/pkg/openrpcmanager/schema.go +++ /dev/null @@ -1,117 +0,0 @@ -package openrpcmanager - -// OpenRPCSchema represents the OpenRPC specification document -// Based on OpenRPC Specification 1.2.6: https://spec.open-rpc.org/ -type OpenRPCSchema struct { - OpenRPC string `json:"openrpc"` // Required: Version of the OpenRPC specification - Info InfoObject `json:"info"` // Required: Information about the API - Methods []MethodObject `json:"methods"` // Required: List of method objects - ExternalDocs *ExternalDocsObject `json:"externalDocs,omitempty"` // Optional: External documentation - Servers []ServerObject `json:"servers,omitempty"` // Optional: List of servers - Components *ComponentsObject `json:"components,omitempty"` // Optional: Reusable components -} - -// InfoObject provides metadata about the API -type InfoObject struct { - Title string `json:"title"` // Required: Title of the API - Description string `json:"description,omitempty"` // Optional: Description of the API - Version string `json:"version"` // Required: Version of the API - TermsOfService string `json:"termsOfService,omitempty"` // Optional: Terms of service URL - Contact *ContactObject `json:"contact,omitempty"` // Optional: Contact information - License *LicenseObject `json:"license,omitempty"` // Optional: License information -} - -// ContactObject provides contact information for the API -type ContactObject struct { - Name string `json:"name,omitempty"` // Optional: Name of the contact - URL string `json:"url,omitempty"` // Optional: URL of the contact - Email string `json:"email,omitempty"` // Optional: Email of the contact -} - -// LicenseObject provides license information for the API -type LicenseObject struct { - Name string `json:"name"` // Required: Name of the license - URL string `json:"url,omitempty"` // Optional: URL of the license -} - -// ExternalDocsObject provides a URL to external documentation -type ExternalDocsObject struct { - Description string `json:"description,omitempty"` // Optional: Description of the external docs - URL string `json:"url"` // Required: URL of the external docs -} - -// ServerObject provides connection information to a server -type ServerObject struct { - Name string `json:"name,omitempty"` // Optional: Name of the server - Description string `json:"description,omitempty"` // Optional: Description of the server - URL string `json:"url"` // Required: URL of the server - Variables map[string]ServerVariable `json:"variables,omitempty"` // Optional: Server variables -} - -// ServerVariable is a variable for server URL template substitution -type ServerVariable struct { - Default string `json:"default"` // Required: Default value of the variable - Description string `json:"description,omitempty"` // Optional: Description of the variable - Enum []string `json:"enum,omitempty"` // Optional: Enumeration of possible values -} - -// MethodObject describes an RPC method -type MethodObject struct { - Name string `json:"name"` // Required: Name of the method - Description string `json:"description,omitempty"` // Optional: Description of the method - Summary string `json:"summary,omitempty"` // Optional: Summary of the method - Params []ContentDescriptorObject `json:"params"` // Required: List of parameters - Result *ContentDescriptorObject `json:"result"` // Required: Description of the result - Deprecated bool `json:"deprecated,omitempty"` // Optional: Whether the method is deprecated - Errors []ErrorObject `json:"errors,omitempty"` // Optional: List of possible errors - Tags []TagObject `json:"tags,omitempty"` // Optional: List of tags - ExternalDocs *ExternalDocsObject `json:"externalDocs,omitempty"` // Optional: External documentation - ParamStructure string `json:"paramStructure,omitempty"` // Optional: Structure of the parameters -} - -// ContentDescriptorObject describes the content of a parameter or result -type ContentDescriptorObject struct { - Name string `json:"name"` // Required: Name of the parameter - Description string `json:"description,omitempty"` // Optional: Description of the parameter - Summary string `json:"summary,omitempty"` // Optional: Summary of the parameter - Required bool `json:"required,omitempty"` // Optional: Whether the parameter is required - Deprecated bool `json:"deprecated,omitempty"` // Optional: Whether the parameter is deprecated - Schema SchemaObject `json:"schema"` // Required: JSON Schema of the parameter -} - -// SchemaObject is a JSON Schema definition -// This is a simplified version, in a real implementation you would use a full JSON Schema library -type SchemaObject map[string]interface{} - -// ErrorObject describes an error that may be returned -type ErrorObject struct { - Code int `json:"code"` // Required: Error code - Message string `json:"message"` // Required: Error message - Data interface{} `json:"data,omitempty"` // Optional: Additional error data -} - -// TagObject describes a tag for documentation purposes -type TagObject struct { - Name string `json:"name"` // Required: Name of the tag - Description string `json:"description,omitempty"` // Optional: Description of the tag - ExternalDocs *ExternalDocsObject `json:"externalDocs,omitempty"` // Optional: External documentation -} - -// ComponentsObject holds reusable objects for different aspects of the OpenRPC spec -type ComponentsObject struct { - Schemas map[string]SchemaObject `json:"schemas,omitempty"` // Optional: Reusable schemas - ContentDescriptors map[string]ContentDescriptorObject `json:"contentDescriptors,omitempty"` // Optional: Reusable content descriptors - Examples map[string]interface{} `json:"examples,omitempty"` // Optional: Reusable examples - Links map[string]LinkObject `json:"links,omitempty"` // Optional: Reusable links - Errors map[string]ErrorObject `json:"errors,omitempty"` // Optional: Reusable errors -} - -// LinkObject describes a link between operations -type LinkObject struct { - Name string `json:"name,omitempty"` // Optional: Name of the link - Description string `json:"description,omitempty"` // Optional: Description of the link - Summary string `json:"summary,omitempty"` // Optional: Summary of the link - Method string `json:"method"` // Required: Method name - Params map[string]interface{} `json:"params,omitempty"` // Optional: Parameters for the method - Server *ServerObject `json:"server,omitempty"` // Optional: Server for the method -} diff --git a/pkg/openrpcmanager/unixserver.go b/pkg/openrpcmanager/unixserver.go deleted file mode 100644 index 3603000..0000000 --- a/pkg/openrpcmanager/unixserver.go +++ /dev/null @@ -1,241 +0,0 @@ -package openrpcmanager - -import ( - "encoding/json" - "fmt" - "io" - "net" - "os" - "path/filepath" - "sync" -) - -// RPCRequest represents an incoming RPC request -type RPCRequest struct { - Method string `json:"method"` - Params json.RawMessage `json:"params"` - ID interface{} `json:"id,omitempty"` - Secret string `json:"secret,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -// RPCResponse represents an outgoing RPC response -type RPCResponse struct { - Result interface{} `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` - ID interface{} `json:"id,omitempty"` - JSONRPC string `json:"jsonrpc"` -} - -// RPCError represents an RPC error -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -// UnixServer represents a Unix socket server for the OpenRPC manager -type UnixServer struct { - manager *OpenRPCManager - socketPath string - listener net.Listener - connections map[net.Conn]bool - mutex sync.Mutex - wg sync.WaitGroup - done chan struct{} -} - -// NewUnixServer creates a new Unix socket server for the OpenRPC manager -func NewUnixServer(manager *OpenRPCManager, socketPath string) (*UnixServer, error) { - // Create directory if it doesn't exist - dir := filepath.Dir(socketPath) - if err := os.MkdirAll(dir, 0755); err != nil { - return nil, fmt.Errorf("failed to create socket directory: %w", err) - } - - // Remove socket if it already exists - if _, err := os.Stat(socketPath); err == nil { - if err := os.Remove(socketPath); err != nil { - return nil, fmt.Errorf("failed to remove existing socket: %w", err) - } - } - - return &UnixServer{ - manager: manager, - socketPath: socketPath, - connections: make(map[net.Conn]bool), - done: make(chan struct{}), - }, nil -} - -// Start starts the Unix socket server -func (s *UnixServer) Start() error { - listener, err := net.Listen("unix", s.socketPath) - if err != nil { - return fmt.Errorf("failed to listen on unix socket: %w", err) - } - s.listener = listener - - // Set socket permissions - if err := os.Chmod(s.socketPath, 0660); err != nil { - s.listener.Close() - return fmt.Errorf("failed to set socket permissions: %w", err) - } - - s.wg.Add(1) - go s.acceptConnections() - - return nil -} - -// Stop stops the Unix socket server -func (s *UnixServer) Stop() error { - close(s.done) - - // Close the listener - if s.listener != nil { - s.listener.Close() - } - - // Close all connections - s.mutex.Lock() - for conn := range s.connections { - conn.Close() - } - s.mutex.Unlock() - - // Wait for all goroutines to finish - s.wg.Wait() - - // Remove the socket file - os.Remove(s.socketPath) - - return nil -} - -// acceptConnections accepts incoming connections -func (s *UnixServer) acceptConnections() { - defer s.wg.Done() - - for { - select { - case <-s.done: - return - default: - conn, err := s.listener.Accept() - if err != nil { - select { - case <-s.done: - return - default: - fmt.Printf("Error accepting connection: %v\n", err) - continue - } - } - - s.mutex.Lock() - s.connections[conn] = true - s.mutex.Unlock() - - s.wg.Add(1) - go s.handleConnection(conn) - } - } -} - -// handleConnection handles a client connection -func (s *UnixServer) handleConnection(conn net.Conn) { - defer func() { - s.mutex.Lock() - delete(s.connections, conn) - s.mutex.Unlock() - conn.Close() - s.wg.Done() - }() - - buf := make([]byte, 4096) - for { - select { - case <-s.done: - return - default: - n, err := conn.Read(buf) - if err != nil { - if err != io.EOF { - fmt.Printf("Error reading from connection: %v\n", err) - } - return - } - - if n > 0 { - go s.handleRequest(conn, buf[:n]) - } - } - } -} - -// handleRequest processes an RPC request -func (s *UnixServer) handleRequest(conn net.Conn, data []byte) { - var req RPCRequest - if err := json.Unmarshal(data, &req); err != nil { - s.sendErrorResponse(conn, nil, -32700, "Parse error", err) - return - } - - // Validate JSON-RPC version - if req.JSONRPC != "2.0" { - s.sendErrorResponse(conn, req.ID, -32600, "Invalid Request", "Invalid JSON-RPC version") - return - } - - var result interface{} - var err error - - // Check if authentication is required - if req.Secret != "" { - result, err = s.manager.HandleRequestWithAuthentication(req.Method, req.Params, req.Secret) - } else { - result, err = s.manager.HandleRequest(req.Method, req.Params) - } - - if err != nil { - s.sendErrorResponse(conn, req.ID, -32603, "Internal error", err.Error()) - return - } - - // Send success response - response := RPCResponse{ - Result: result, - ID: req.ID, - JSONRPC: "2.0", - } - - responseData, err := json.Marshal(response) - if err != nil { - s.sendErrorResponse(conn, req.ID, -32603, "Internal error", err.Error()) - return - } - - conn.Write(responseData) -} - -// sendErrorResponse sends an error response -func (s *UnixServer) sendErrorResponse(conn net.Conn, id interface{}, code int, message string, data interface{}) { - response := RPCResponse{ - Error: &RPCError{ - Code: code, - Message: message, - Data: data, - }, - ID: id, - JSONRPC: "2.0", - } - - responseData, err := json.Marshal(response) - if err != nil { - fmt.Printf("Error marshaling error response: %v\n", err) - return - } - - conn.Write(responseData) -} diff --git a/pkg/openrpcmanager/unixserver_test.go b/pkg/openrpcmanager/unixserver_test.go deleted file mode 100644 index 458f29c..0000000 --- a/pkg/openrpcmanager/unixserver_test.go +++ /dev/null @@ -1,362 +0,0 @@ -package openrpcmanager - -import ( - "encoding/json" - "fmt" - "net" - "os" - "path/filepath" - "testing" - "time" -) - -func TestUnixServer(t *testing.T) { - // Create a temporary socket path - tempDir, err := os.MkdirTemp("", "openrpc-test") - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - defer os.RemoveAll(tempDir) - - socketPath := filepath.Join(tempDir, "openrpc.sock") - - // Create OpenRPC manager - schema := createTestSchema() - handlers := createTestHandlers() - manager, err := NewOpenRPCManager(schema, handlers, "test-secret") - if err != nil { - t.Fatalf("Failed to create OpenRPCManager: %v", err) - } - - // Create and start Unix server - server, err := NewUnixServer(manager, socketPath) - if err != nil { - t.Fatalf("Failed to create UnixServer: %v", err) - } - - if err := server.Start(); err != nil { - t.Fatalf("Failed to start UnixServer: %v", err) - } - defer server.Stop() - - // Wait for server to start - time.Sleep(100 * time.Millisecond) - - // Test connection - conn, err := net.Dial("unix", socketPath) - if err != nil { - t.Fatalf("Failed to connect to Unix socket: %v", err) - } - defer conn.Close() - - // Test echo method - t.Run("Echo method", func(t *testing.T) { - request := RPCRequest{ - Method: "echo", - Params: json.RawMessage(`{"message":"hello world"}`), - ID: 1, - JSONRPC: "2.0", - } - - requestData, err := json.Marshal(request) - if err != nil { - t.Fatalf("Failed to marshal request: %v", err) - } - - _, err = conn.Write(requestData) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - // Read response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - t.Fatalf("Failed to read response: %v", err) - } - - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Check response - if response.Error != nil { - t.Fatalf("Received error response: %v", response.Error) - } - - // Note: JSON unmarshaling may convert numbers to float64, so we need to check the value not exact type - if fmt.Sprintf("%v", response.ID) != fmt.Sprintf("%v", request.ID) { - t.Errorf("Response ID mismatch. Expected: %v, Got: %v", request.ID, response.ID) - } - - // Check result - resultMap, ok := response.Result.(map[string]interface{}) - if !ok { - t.Fatalf("Expected map result, got: %T", response.Result) - } - - if resultMap["message"] != "hello world" { - t.Errorf("Expected 'hello world', got: %v", resultMap["message"]) - } - }) - - // Test add method - t.Run("Add method", func(t *testing.T) { - request := RPCRequest{ - Method: "add", - Params: json.RawMessage(`{"a":5,"b":7}`), - ID: 2, - JSONRPC: "2.0", - } - - requestData, err := json.Marshal(request) - if err != nil { - t.Fatalf("Failed to marshal request: %v", err) - } - - _, err = conn.Write(requestData) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - // Read response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - t.Fatalf("Failed to read response: %v", err) - } - - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Check response - if response.Error != nil { - t.Fatalf("Received error response: %v", response.Error) - } - - // Note: JSON unmarshaling may convert numbers to float64, so we need to check the value not exact type - if fmt.Sprintf("%v", response.ID) != fmt.Sprintf("%v", request.ID) { - t.Errorf("Response ID mismatch. Expected: %v, Got: %v", request.ID, response.ID) - } - - // Check result - resultValue, ok := response.Result.(float64) - if !ok { - t.Fatalf("Expected float64 result, got: %T", response.Result) - } - - if resultValue != float64(12) { - t.Errorf("Expected 12, got: %v", resultValue) - } - }) - - // Test authenticated method - t.Run("Authenticated method", func(t *testing.T) { - request := RPCRequest{ - Method: "secure.method", - Params: json.RawMessage(`{}`), - ID: 3, - Secret: "test-secret", - JSONRPC: "2.0", - } - - requestData, err := json.Marshal(request) - if err != nil { - t.Fatalf("Failed to marshal request: %v", err) - } - - _, err = conn.Write(requestData) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - // Read response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - t.Fatalf("Failed to read response: %v", err) - } - - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Check response - if response.Error != nil { - t.Fatalf("Received error response: %v", response.Error) - } - - // Note: JSON unmarshaling may convert numbers to float64, so we need to check the value not exact type - if fmt.Sprintf("%v", response.ID) != fmt.Sprintf("%v", request.ID) { - t.Errorf("Response ID mismatch. Expected: %v, Got: %v", request.ID, response.ID) - } - - // Check result - resultValue, ok := response.Result.(string) - if !ok { - t.Fatalf("Expected string result, got: %T", response.Result) - } - - if resultValue != "secure data" { - t.Errorf("Expected 'secure data', got: %v", resultValue) - } - }) - - // Test authentication failure - t.Run("Authentication failure", func(t *testing.T) { - request := RPCRequest{ - Method: "secure.method", - Params: json.RawMessage(`{}`), - ID: 4, - Secret: "wrong-secret", - JSONRPC: "2.0", - } - - requestData, err := json.Marshal(request) - if err != nil { - t.Fatalf("Failed to marshal request: %v", err) - } - - _, err = conn.Write(requestData) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - // Read response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - t.Fatalf("Failed to read response: %v", err) - } - - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Check response - if response.Error == nil { - t.Fatal("Expected error response, but got nil") - } - - // Note: JSON unmarshaling may convert numbers to float64, so we need to check the value not exact type - if fmt.Sprintf("%v", response.ID) != fmt.Sprintf("%v", request.ID) { - t.Errorf("Response ID mismatch. Expected: %v, Got: %v", request.ID, response.ID) - } - - if response.Error.Code != -32603 { - t.Errorf("Expected error code -32603, got: %v", response.Error.Code) - } - }) - - // Test non-existent method - t.Run("Non-existent method", func(t *testing.T) { - request := RPCRequest{ - Method: "nonexistent", - Params: json.RawMessage(`{}`), - ID: 5, - JSONRPC: "2.0", - } - - requestData, err := json.Marshal(request) - if err != nil { - t.Fatalf("Failed to marshal request: %v", err) - } - - _, err = conn.Write(requestData) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - // Read response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - t.Fatalf("Failed to read response: %v", err) - } - - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Check response - if response.Error == nil { - t.Fatal("Expected error response, but got nil") - } - - // Note: JSON unmarshaling may convert numbers to float64, so we need to check the value not exact type - if fmt.Sprintf("%v", response.ID) != fmt.Sprintf("%v", request.ID) { - t.Errorf("Response ID mismatch. Expected: %v, Got: %v", request.ID, response.ID) - } - - if response.Error.Code != -32603 { - t.Errorf("Expected error code -32603, got: %v", response.Error.Code) - } - }) - - // Test discovery method - t.Run("Discovery method", func(t *testing.T) { - request := RPCRequest{ - Method: "rpc.discover", - Params: json.RawMessage(`{}`), - ID: 6, - JSONRPC: "2.0", - } - - requestData, err := json.Marshal(request) - if err != nil { - t.Fatalf("Failed to marshal request: %v", err) - } - - _, err = conn.Write(requestData) - if err != nil { - t.Fatalf("Failed to send request: %v", err) - } - - // Read response - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - t.Fatalf("Failed to read response: %v", err) - } - - var response RPCResponse - if err := json.Unmarshal(buf[:n], &response); err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Check response - if response.Error != nil { - t.Fatalf("Received error response: %v", response.Error) - } - - // Note: JSON unmarshaling may convert numbers to float64, so we need to check the value not exact type - if fmt.Sprintf("%v", response.ID) != fmt.Sprintf("%v", request.ID) { - t.Errorf("Response ID mismatch. Expected: %v, Got: %v", request.ID, response.ID) - } - - // Check that we got a valid schema - resultMap, ok := response.Result.(map[string]interface{}) - if !ok { - t.Fatalf("Expected map result, got: %T", response.Result) - } - - if resultMap["openrpc"] != "1.2.6" { - t.Errorf("Expected OpenRPC version 1.2.6, got: %v", resultMap["openrpc"]) - } - - methods, ok := resultMap["methods"].([]interface{}) - if !ok { - t.Fatalf("Expected methods array, got: %T", resultMap["methods"]) - } - - if len(methods) < 3 { - t.Errorf("Expected at least 3 methods, got: %d", len(methods)) - } - }) -}