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, ¶ms); 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, ¶ms); 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