...
This commit is contained in:
		
							
								
								
									
										209
									
								
								pkg/openrpcmanager/client/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								pkg/openrpcmanager/client/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/herolauncher/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 | ||||
| } | ||||
							
								
								
									
										283
									
								
								pkg/openrpcmanager/client/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								pkg/openrpcmanager/client/client_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/herolauncher/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") | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										113
									
								
								pkg/openrpcmanager/cmd/server/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								pkg/openrpcmanager/cmd/server/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/freeflowuniverse/herolauncher/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...") | ||||
| } | ||||
							
								
								
									
										112
									
								
								pkg/openrpcmanager/examples/unixclient/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								pkg/openrpcmanager/examples/unixclient/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| 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)) | ||||
| } | ||||
							
								
								
									
										416
									
								
								pkg/openrpcmanager/openrpcmanager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								pkg/openrpcmanager/openrpcmanager.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,416 @@ | ||||
| // 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, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										446
									
								
								pkg/openrpcmanager/openrpcmanager_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										446
									
								
								pkg/openrpcmanager/openrpcmanager_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,446 @@ | ||||
| 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") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										117
									
								
								pkg/openrpcmanager/schema.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								pkg/openrpcmanager/schema.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										241
									
								
								pkg/openrpcmanager/unixserver.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								pkg/openrpcmanager/unixserver.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| 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) | ||||
| } | ||||
							
								
								
									
										362
									
								
								pkg/openrpcmanager/unixserver_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								pkg/openrpcmanager/unixserver_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,362 @@ | ||||
| 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)) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user