...
This commit is contained in:
parent
b2eb9d3116
commit
c86165f88c
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
@ -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...")
|
||||
}
|
@ -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))
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user