session.go raw

   1  package bunker
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"fmt"
   7  	"time"
   8  
   9  	"github.com/gorilla/websocket"
  10  	"lukechampine.com/frand"
  11  	"next.orly.dev/pkg/lol/log"
  12  
  13  	"next.orly.dev/pkg/nostr/encoders/event"
  14  	"next.orly.dev/pkg/nostr/encoders/hex"
  15  	"next.orly.dev/pkg/nostr/encoders/timestamp"
  16  	"next.orly.dev/pkg/nostr/interfaces/signer"
  17  )
  18  
  19  // NIP-46 method names
  20  const (
  21  	MethodConnect      = "connect"
  22  	MethodGetPublicKey = "get_public_key"
  23  	MethodSignEvent    = "sign_event"
  24  	MethodNIP04Encrypt = "nip04_encrypt"
  25  	MethodNIP04Decrypt = "nip04_decrypt"
  26  	MethodNIP44Encrypt = "nip44_encrypt"
  27  	MethodNIP44Decrypt = "nip44_decrypt"
  28  	MethodPing         = "ping"
  29  )
  30  
  31  // Session represents a NIP-46 client session.
  32  type Session struct {
  33  	ID            string
  34  	conn          *websocket.Conn
  35  	ctx           context.Context
  36  	cancel        context.CancelFunc
  37  	relaySigner   signer.I
  38  	relayPubkey   []byte
  39  	authenticated bool
  40  	clientPubkey  []byte // Client's pubkey after connect
  41  }
  42  
  43  // NewSession creates a new bunker session.
  44  func NewSession(parentCtx context.Context, conn *websocket.Conn, relaySigner signer.I, relayPubkey []byte) *Session {
  45  	ctx, cancel := context.WithCancel(parentCtx)
  46  
  47  	// Generate random session ID
  48  	idBytes := make([]byte, 16)
  49  	frand.Read(idBytes)
  50  
  51  	return &Session{
  52  		ID:          hex.Enc(idBytes),
  53  		conn:        conn,
  54  		ctx:         ctx,
  55  		cancel:      cancel,
  56  		relaySigner: relaySigner,
  57  		relayPubkey: relayPubkey,
  58  	}
  59  }
  60  
  61  // Handle processes messages from the client.
  62  func (s *Session) Handle() {
  63  	defer s.conn.Close()
  64  	defer s.cancel()
  65  
  66  	log.D.F("bunker session started: %s", s.ID[:8])
  67  
  68  	for {
  69  		select {
  70  		case <-s.ctx.Done():
  71  			return
  72  		default:
  73  		}
  74  
  75  		// Set read deadline
  76  		s.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
  77  
  78  		// Read message
  79  		_, msg, err := s.conn.ReadMessage()
  80  		if err != nil {
  81  			if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
  82  				log.D.F("bunker session closed normally: %s", s.ID[:8])
  83  			} else {
  84  				log.D.F("bunker session read error: %v", err)
  85  			}
  86  			return
  87  		}
  88  
  89  		// Parse request
  90  		var req NIP46Request
  91  		if err := json.Unmarshal(msg, &req); err != nil {
  92  			s.sendError("", "invalid request format")
  93  			continue
  94  		}
  95  
  96  		// Handle request
  97  		resp := s.handleRequest(&req)
  98  		s.sendResponse(resp)
  99  	}
 100  }
 101  
 102  // handleRequest processes a NIP-46 request.
 103  func (s *Session) handleRequest(req *NIP46Request) *NIP46Response {
 104  	switch req.Method {
 105  	case MethodConnect:
 106  		return s.handleConnect(req)
 107  	case MethodGetPublicKey:
 108  		return s.handleGetPublicKey(req)
 109  	case MethodSignEvent:
 110  		return s.handleSignEvent(req)
 111  	case MethodPing:
 112  		return s.handlePing(req)
 113  	case MethodNIP44Encrypt, MethodNIP44Decrypt, MethodNIP04Encrypt, MethodNIP04Decrypt:
 114  		// Encryption/decryption not supported in this bunker implementation
 115  		return &NIP46Response{
 116  			ID:    req.ID,
 117  			Error: "encryption methods not supported",
 118  		}
 119  	default:
 120  		return &NIP46Response{
 121  			ID:    req.ID,
 122  			Error: fmt.Sprintf("unsupported method: %s", req.Method),
 123  		}
 124  	}
 125  }
 126  
 127  // handleConnect handles the connect method.
 128  func (s *Session) handleConnect(req *NIP46Request) *NIP46Response {
 129  	// Parse params: [pubkey, secret?]
 130  	var params []string
 131  	if err := json.Unmarshal(req.Params, &params); err != nil {
 132  		return &NIP46Response{ID: req.ID, Error: "invalid params"}
 133  	}
 134  
 135  	if len(params) < 1 {
 136  		return &NIP46Response{ID: req.ID, Error: "missing pubkey"}
 137  	}
 138  
 139  	pubkeyHex := params[0]
 140  	clientPubkey, err := hex.Dec(pubkeyHex)
 141  	if err != nil || len(clientPubkey) != 32 {
 142  		return &NIP46Response{ID: req.ID, Error: "invalid pubkey"}
 143  	}
 144  
 145  	s.clientPubkey = clientPubkey
 146  	s.authenticated = true
 147  
 148  	log.D.F("bunker session authenticated: %s (client=%s...)",
 149  		s.ID[:8], pubkeyHex[:16])
 150  
 151  	return &NIP46Response{
 152  		ID:     req.ID,
 153  		Result: "ack",
 154  	}
 155  }
 156  
 157  // handleGetPublicKey returns the relay's public key.
 158  func (s *Session) handleGetPublicKey(req *NIP46Request) *NIP46Response {
 159  	return &NIP46Response{
 160  		ID:     req.ID,
 161  		Result: hex.Enc(s.relayPubkey),
 162  	}
 163  }
 164  
 165  // handleSignEvent signs an event with the relay's key.
 166  func (s *Session) handleSignEvent(req *NIP46Request) *NIP46Response {
 167  	if !s.authenticated {
 168  		return &NIP46Response{ID: req.ID, Error: "not authenticated"}
 169  	}
 170  
 171  	// Parse event from params
 172  	var params []json.RawMessage
 173  	if err := json.Unmarshal(req.Params, &params); err != nil {
 174  		return &NIP46Response{ID: req.ID, Error: "invalid params"}
 175  	}
 176  
 177  	if len(params) < 1 {
 178  		return &NIP46Response{ID: req.ID, Error: "missing event"}
 179  	}
 180  
 181  	// Parse the event
 182  	ev := &event.E{}
 183  	if err := json.Unmarshal(params[0], ev); err != nil {
 184  		return &NIP46Response{ID: req.ID, Error: "invalid event"}
 185  	}
 186  
 187  	// Set pubkey to relay's pubkey
 188  	copy(ev.Pubkey[:], s.relayPubkey)
 189  
 190  	// Set created_at if not set
 191  	if ev.CreatedAt == 0 {
 192  		ev.CreatedAt = timestamp.Now().V
 193  	}
 194  
 195  	// Sign the event
 196  	if err := ev.Sign(s.relaySigner); err != nil {
 197  		return &NIP46Response{ID: req.ID, Error: fmt.Sprintf("signing failed: %v", err)}
 198  	}
 199  
 200  	// Return signed event as JSON
 201  	signedJSON, err := json.Marshal(ev)
 202  	if err != nil {
 203  		return &NIP46Response{ID: req.ID, Error: "marshal failed"}
 204  	}
 205  
 206  	return &NIP46Response{
 207  		ID:     req.ID,
 208  		Result: string(signedJSON),
 209  	}
 210  }
 211  
 212  // handlePing responds to ping requests.
 213  func (s *Session) handlePing(req *NIP46Request) *NIP46Response {
 214  	return &NIP46Response{
 215  		ID:     req.ID,
 216  		Result: "pong",
 217  	}
 218  }
 219  
 220  // sendResponse sends a response to the client.
 221  func (s *Session) sendResponse(resp *NIP46Response) {
 222  	data, err := json.Marshal(resp)
 223  	if err != nil {
 224  		log.E.F("bunker marshal error: %v", err)
 225  		return
 226  	}
 227  
 228  	s.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
 229  	if err := s.conn.WriteMessage(websocket.TextMessage, data); err != nil {
 230  		log.E.F("bunker write error: %v", err)
 231  	}
 232  }
 233  
 234  // sendError sends an error response.
 235  func (s *Session) sendError(id, msg string) {
 236  	s.sendResponse(&NIP46Response{
 237  		ID:    id,
 238  		Error: msg,
 239  	})
 240  }
 241