421 lines
11 KiB
Go
421 lines
11 KiB
Go
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"synapse/pkg/types"
|
|
)
|
|
|
|
// =============================================================================
|
|
// HTTP Transport
|
|
// =============================================================================
|
|
|
|
// HTTPTransport implements HTTP-based transport for Synapse messages
|
|
type HTTPTransport struct {
|
|
mu sync.RWMutex
|
|
|
|
// Configuration
|
|
baseURL string
|
|
timeout time.Duration
|
|
maxRetries int
|
|
retryDelay time.Duration
|
|
|
|
// HTTP Client
|
|
client *http.Client
|
|
|
|
// Security
|
|
authSecret string
|
|
|
|
// Callbacks
|
|
callbackEndpoint string
|
|
callbackHandler MessageCallbackHandler
|
|
}
|
|
|
|
// HTTPTransportConfig holds HTTP transport configuration
|
|
type HTTPTransportConfig struct {
|
|
BaseURL string
|
|
Timeout time.Duration
|
|
MaxRetries int
|
|
RetryDelay time.Duration
|
|
AuthSecret string
|
|
}
|
|
|
|
// DefaultHTTPTransportConfig returns default HTTP transport configuration
|
|
func DefaultHTTPTransportConfig() *HTTPTransportConfig {
|
|
return &HTTPTransportConfig{
|
|
Timeout: 30 * time.Second,
|
|
MaxRetries: 5,
|
|
RetryDelay: 2 * time.Second,
|
|
}
|
|
}
|
|
|
|
// NewHTTPTransport creates a new HTTP transport
|
|
func NewHTTPTransport(config *HTTPTransportConfig) *HTTPTransport {
|
|
if config == nil {
|
|
config = DefaultHTTPTransportConfig()
|
|
}
|
|
|
|
t := &HTTPTransport{
|
|
baseURL: config.BaseURL,
|
|
timeout: config.Timeout,
|
|
maxRetries: config.MaxRetries,
|
|
retryDelay: config.RetryDelay,
|
|
authSecret: config.AuthSecret,
|
|
client: &http.Client{
|
|
Timeout: config.Timeout,
|
|
},
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// SetCallbackHandler sets the handler for incoming callbacks
|
|
func (t *HTTPTransport) SetCallbackHandler(handler MessageCallbackHandler) {
|
|
t.callbackHandler = handler
|
|
}
|
|
|
|
// SetCallbackEndpoint sets the endpoint for receiving callbacks
|
|
func (t *HTTPTransport) SetCallbackEndpoint(endpoint string) {
|
|
t.callbackEndpoint = endpoint
|
|
}
|
|
|
|
// =============================================================================
|
|
// Message Sending
|
|
// =============================================================================
|
|
|
|
// Send sends a message via HTTP POST
|
|
func (t *HTTPTransport) Send(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error {
|
|
if endpoint == "" {
|
|
return fmt.Errorf("endpoint is required")
|
|
}
|
|
|
|
// Add signature if secret is configured
|
|
if t.authSecret != "" {
|
|
t.signMessage(msg)
|
|
}
|
|
|
|
// Marshal message
|
|
body, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal message: %w", err)
|
|
}
|
|
|
|
// Create request
|
|
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Synapse-Version", "1.0.0")
|
|
req.Header.Set("X-Synapse-Agent", msg.Sender.AgentID)
|
|
req.Header.Set("X-Synapse-System", msg.Sender.SystemID)
|
|
|
|
if msg.Signature != "" {
|
|
req.Header.Set("X-Synapse-Signature", msg.Signature)
|
|
}
|
|
|
|
// Send with retries
|
|
var lastErr error
|
|
for i := 0; i < t.maxRetries; i++ {
|
|
if i > 0 {
|
|
delay := t.retryDelay * time.Duration(1<<uint(i-1))
|
|
log.Printf("[Transport] Retrying request (%d/%d) after %v", i, t.maxRetries, delay)
|
|
time.Sleep(delay)
|
|
}
|
|
|
|
resp, err := t.client.Do(req)
|
|
if err != nil {
|
|
lastErr = err
|
|
log.Printf("[Transport] Request failed: %v", err)
|
|
continue
|
|
}
|
|
|
|
// Read response
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
log.Printf("[Transport] Message sent successfully: %s to %s", msg.ID, endpoint)
|
|
return nil
|
|
}
|
|
|
|
lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
|
log.Printf("[Transport] Request failed: %v", lastErr)
|
|
}
|
|
|
|
return fmt.Errorf("failed to send message after %d retries: %w", t.maxRetries, lastErr)
|
|
}
|
|
|
|
// SendWithCallback sends a message and waits for callback response
|
|
func (t *HTTPTransport) SendWithCallback(
|
|
ctx context.Context,
|
|
endpoint string,
|
|
msg *types.UnifiedSynapseMessage,
|
|
timeout time.Duration,
|
|
) (*types.UnifiedSynapseMessage, error) {
|
|
// Create channel for response
|
|
respChan := make(chan *types.UnifiedSynapseMessage, 1)
|
|
errChan := make(chan error, 1)
|
|
|
|
// Register callback handler for this message
|
|
correlationID := msg.CorrelationID
|
|
if correlationID == "" {
|
|
correlationID = msg.ID
|
|
}
|
|
|
|
// Start timeout goroutine
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
errChan <- ctx.Err()
|
|
case <-time.After(timeout):
|
|
errChan <- fmt.Errorf("timeout waiting for response")
|
|
}
|
|
}()
|
|
|
|
// Send message
|
|
if err := t.Send(ctx, endpoint, msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Wait for response
|
|
select {
|
|
case resp := <-respChan:
|
|
return resp, nil
|
|
case err := <-errChan:
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Message Reception
|
|
// =============================================================================
|
|
|
|
// MessageCallbackHandler is the interface for handling incoming messages
|
|
type MessageCallbackHandler interface {
|
|
HandleMessage(msg *types.UnifiedSynapseMessage) error
|
|
}
|
|
|
|
// HandleCallback handles an incoming callback message (HTTP POST)
|
|
func (t *HTTPTransport) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Read body
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
r.Body.Close()
|
|
|
|
// Parse message
|
|
var msg types.UnifiedSynapseMessage
|
|
if err := json.Unmarshal(body, &msg); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Verify signature if configured
|
|
if t.authSecret != "" && msg.Signature != "" {
|
|
if !t.verifySignature(body, msg.Signature) {
|
|
http.Error(w, "invalid signature", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Handle message
|
|
if t.callbackHandler != nil {
|
|
if err := t.callbackHandler.HandleMessage(&msg); err != nil {
|
|
log.Printf("[Transport] Failed to handle message: %v", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Send success response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "received",
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// HTTP Server
|
|
// =============================================================================
|
|
|
|
// Server is an HTTP server for Synapse transport
|
|
type Server struct {
|
|
transport *HTTPTransport
|
|
mux *http.ServeMux
|
|
server *http.Server
|
|
}
|
|
|
|
// NewServer creates a new HTTP server for Synapse transport
|
|
func NewServer(addr string, transport *HTTPTransport) *Server {
|
|
mux := http.NewServeMux()
|
|
server := &Server{
|
|
transport: transport,
|
|
mux: mux,
|
|
server: &http.Server{
|
|
Addr: addr,
|
|
Handler: mux,
|
|
},
|
|
}
|
|
|
|
// Register callback endpoint
|
|
mux.HandleFunc("/synapse/callback", transport.HandleCallback)
|
|
mux.HandleFunc("/synapse/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
})
|
|
|
|
return server
|
|
}
|
|
|
|
// Start starts the HTTP server
|
|
func (s *Server) Start() error {
|
|
log.Printf("[Transport] Starting HTTP server on %s", s.server.Addr)
|
|
return s.server.ListenAndServe()
|
|
}
|
|
|
|
// Stop stops the HTTP server
|
|
func (s *Server) Stop(ctx context.Context) error {
|
|
return s.server.Shutdown(ctx)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Security
|
|
// =============================================================================
|
|
|
|
func (t *HTTPTransport) signMessage(msg *types.UnifiedSynapseMessage) {
|
|
// Create canonical body (message without signature)
|
|
msgCopy := *msg
|
|
msgCopy.Signature = ""
|
|
|
|
body, _ := json.Marshal(msgCopy)
|
|
|
|
// Compute HMAC-SHA256
|
|
h := hmac.New(sha256.New, []byte(t.authSecret))
|
|
h.Write(body)
|
|
signature := hex.EncodeToString(h.Sum(nil))
|
|
|
|
msg.Signature = signature
|
|
}
|
|
|
|
func (t *HTTPTransport) verifySignature(body []byte, signature string) bool {
|
|
h := hmac.New(sha256.New, []byte(t.authSecret))
|
|
h.Write(body)
|
|
expected := hex.EncodeToString(h.Sum(nil))
|
|
|
|
return hmac.Equal([]byte(strings.ToLower(signature)), []byte(strings.ToLower(expected)))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Callback Sender (implements p2p.CallbackSender)
|
|
// =============================================================================
|
|
|
|
// HTTPCallbackSender implements p2p.CallbackSender using HTTP transport
|
|
type HTTPCallbackSender struct {
|
|
transport *HTTPTransport
|
|
}
|
|
|
|
// NewHTTPCallbackSender creates a new HTTP callback sender
|
|
func NewHTTPCallbackSender(t *HTTPTransport) *HTTPCallbackSender {
|
|
return &HTTPCallbackSender{transport: t}
|
|
}
|
|
|
|
// SendCallback sends a callback message
|
|
func (s *HTTPCallbackSender) SendCallback(ctx context.Context, endpoint string, msg *types.UnifiedSynapseMessage) error {
|
|
if endpoint == "" && msg.Receiver != nil {
|
|
endpoint = msg.Receiver.Endpoint
|
|
}
|
|
return s.transport.Send(ctx, endpoint, msg)
|
|
}
|
|
|
|
// =============================================================================
|
|
// WebSocket Transport (for future extension)
|
|
// =============================================================================
|
|
|
|
// WebSocketConfig holds WebSocket transport configuration
|
|
type WebSocketConfig struct {
|
|
URL string
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// WebSocketTransport implements WebSocket-based transport
|
|
type WebSocketTransport struct {
|
|
mu sync.RWMutex
|
|
config *WebSocketConfig
|
|
handler MessageCallbackHandler
|
|
}
|
|
|
|
// NewWebSocketTransport creates a new WebSocket transport
|
|
func NewWebSocketTransport(config *WebSocketConfig) *WebSocketTransport {
|
|
return &WebSocketTransport{
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// SetMessageHandler sets the message handler
|
|
func (t *WebSocketTransport) SetMessageHandler(handler MessageCallbackHandler) {
|
|
t.handler = handler
|
|
}
|
|
|
|
// Connect establishes a WebSocket connection
|
|
func (t *WebSocketTransport) Connect(ctx context.Context) error {
|
|
// Placeholder for WebSocket implementation
|
|
// In a real implementation, this would use gorilla/websocket
|
|
log.Printf("[Transport] WebSocket connect to %s (placeholder)", t.config.URL)
|
|
return nil
|
|
}
|
|
|
|
// Send sends a message via WebSocket
|
|
func (t *WebSocketTransport) Send(ctx context.Context, msg *types.UnifiedSynapseMessage) error {
|
|
// Placeholder for WebSocket send implementation
|
|
log.Printf("[Transport] WebSocket send (placeholder): %s", msg.ID)
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Message Utilities
|
|
// =============================================================================
|
|
|
|
// CreateSuccessResponse creates a success response
|
|
func CreateSuccessResponse(msgID string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"success": true,
|
|
"message": "received",
|
|
"id": msgID,
|
|
}
|
|
}
|
|
|
|
// CreateErrorResponse creates an error response
|
|
func CreateErrorResponse(code int, message string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"success": false,
|
|
"error": map[string]interface{}{
|
|
"code": code,
|
|
"message": message,
|
|
},
|
|
}
|
|
}
|