diff options
| author | bndw <ben@bdw.to> | 2026-03-09 12:48:39 -0700 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-03-09 12:48:39 -0700 |
| commit | 0bd392e076e36c80a152abe00cbcd0bc9efedd9c (patch) | |
| tree | 2727cf8a030e675fa86082b56faf9fee4ddc45b1 /relay | |
| parent | 2c1ec8f9e964c2e89eee900299db667d9a58db25 (diff) | |
feat: add axon CLI for pub/sub; fix relay hijack context bug
- cmd/axon: new CLI module with keygen, pub, and req subcommands
- keygen: generate Ed25519 keypair, print hex seed and pubkey
- pub: sign and publish an event; accepts --kind, --content, --tag
- req: query/stream events as JSON lines; accepts --kind, --author,
--tag, --since, --until, --limit, --stream
- key loaded from --key flag or AXON_KEY env var
- relay/websocket: add Dial() for client-side WebSocket handshake
(ws:// and wss://, RFC 6455 masking via client:true)
- relay/server: fix broken-pipe on auth by switching hijacked conn
goroutine from r.Context() to context.Background(); r.Context() is
cancelled by net/http immediately after Hijack is called
Diffstat (limited to 'relay')
| -rw-r--r-- | relay/server.go | 5 | ||||
| -rw-r--r-- | relay/websocket/websocket.go | 75 |
2 files changed, 79 insertions, 1 deletions
diff --git a/relay/server.go b/relay/server.go index 085929c..d4a1edd 100644 --- a/relay/server.go +++ b/relay/server.go | |||
| @@ -98,7 +98,10 @@ func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) { | |||
| 98 | s.mu.Add(1) | 98 | s.mu.Add(1) |
| 99 | go func() { | 99 | go func() { |
| 100 | defer s.mu.Done() | 100 | defer s.mu.Done() |
| 101 | ctx := r.Context() | 101 | // r.Context() is cancelled by the HTTP server when Hijack is called, |
| 102 | // so we use a fresh context. The connection manages its own lifecycle | ||
| 103 | // via the ping loop and WebSocket close frames. | ||
| 104 | ctx := context.Background() | ||
| 102 | h.serve(ctx) | 105 | h.serve(ctx) |
| 103 | if err := c.CloseConn(); err != nil { | 106 | if err := c.CloseConn(); err != nil { |
| 104 | // Ignore close errors — connection may already be gone. | 107 | // Ignore close errors — connection may already be gone. |
diff --git a/relay/websocket/websocket.go b/relay/websocket/websocket.go index cfc3289..2ae0dec 100644 --- a/relay/websocket/websocket.go +++ b/relay/websocket/websocket.go | |||
| @@ -7,12 +7,14 @@ import ( | |||
| 7 | "context" | 7 | "context" |
| 8 | "crypto/rand" | 8 | "crypto/rand" |
| 9 | "crypto/sha1" | 9 | "crypto/sha1" |
| 10 | "crypto/tls" | ||
| 10 | "encoding/base64" | 11 | "encoding/base64" |
| 11 | "encoding/binary" | 12 | "encoding/binary" |
| 12 | "fmt" | 13 | "fmt" |
| 13 | "io" | 14 | "io" |
| 14 | "net" | 15 | "net" |
| 15 | "net/http" | 16 | "net/http" |
| 17 | "net/url" | ||
| 16 | "strings" | 18 | "strings" |
| 17 | "sync" | 19 | "sync" |
| 18 | "time" | 20 | "time" |
| @@ -207,6 +209,79 @@ func acceptKey(key string) string { | |||
| 207 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) | 209 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) |
| 208 | } | 210 | } |
| 209 | 211 | ||
| 212 | // Dial connects to a WebSocket server at rawURL and performs the client-side | ||
| 213 | // RFC 6455 handshake. Supports ws:// and wss:// schemes. | ||
| 214 | func Dial(rawURL string) (*Conn, error) { | ||
| 215 | u, err := url.Parse(rawURL) | ||
| 216 | if err != nil { | ||
| 217 | return nil, fmt.Errorf("websocket: parse url: %w", err) | ||
| 218 | } | ||
| 219 | |||
| 220 | host := u.Host | ||
| 221 | var netConn net.Conn | ||
| 222 | switch u.Scheme { | ||
| 223 | case "ws": | ||
| 224 | if !strings.Contains(host, ":") { | ||
| 225 | host += ":80" | ||
| 226 | } | ||
| 227 | netConn, err = net.Dial("tcp", host) | ||
| 228 | case "wss": | ||
| 229 | if !strings.Contains(host, ":") { | ||
| 230 | host += ":443" | ||
| 231 | } | ||
| 232 | netConn, err = tls.Dial("tcp", host, &tls.Config{ServerName: u.Hostname()}) | ||
| 233 | default: | ||
| 234 | return nil, fmt.Errorf("websocket: unsupported scheme %q (use ws:// or wss://)", u.Scheme) | ||
| 235 | } | ||
| 236 | if err != nil { | ||
| 237 | return nil, fmt.Errorf("websocket: dial %s: %w", host, err) | ||
| 238 | } | ||
| 239 | |||
| 240 | // Generate a random 16-byte key and base64-encode it. | ||
| 241 | var keyBytes [16]byte | ||
| 242 | if _, err := rand.Read(keyBytes[:]); err != nil { | ||
| 243 | netConn.Close() | ||
| 244 | return nil, fmt.Errorf("websocket: generate key: %w", err) | ||
| 245 | } | ||
| 246 | key := base64.StdEncoding.EncodeToString(keyBytes[:]) | ||
| 247 | |||
| 248 | path := u.RequestURI() | ||
| 249 | if path == "" { | ||
| 250 | path = "/" | ||
| 251 | } | ||
| 252 | |||
| 253 | req := "GET " + path + " HTTP/1.1\r\n" + | ||
| 254 | "Host: " + u.Host + "\r\n" + | ||
| 255 | "Upgrade: websocket\r\n" + | ||
| 256 | "Connection: Upgrade\r\n" + | ||
| 257 | "Sec-WebSocket-Key: " + key + "\r\n" + | ||
| 258 | "Sec-WebSocket-Version: 13\r\n\r\n" | ||
| 259 | |||
| 260 | if _, err := netConn.Write([]byte(req)); err != nil { | ||
| 261 | netConn.Close() | ||
| 262 | return nil, fmt.Errorf("websocket: send handshake: %w", err) | ||
| 263 | } | ||
| 264 | |||
| 265 | br := bufio.NewReader(netConn) | ||
| 266 | resp, err := http.ReadResponse(br, nil) | ||
| 267 | if err != nil { | ||
| 268 | netConn.Close() | ||
| 269 | return nil, fmt.Errorf("websocket: read handshake response: %w", err) | ||
| 270 | } | ||
| 271 | resp.Body.Close() | ||
| 272 | |||
| 273 | if resp.StatusCode != 101 { | ||
| 274 | netConn.Close() | ||
| 275 | return nil, fmt.Errorf("websocket: server returned status %d, want 101", resp.StatusCode) | ||
| 276 | } | ||
| 277 | if resp.Header.Get("Sec-WebSocket-Accept") != acceptKey(key) { | ||
| 278 | netConn.Close() | ||
| 279 | return nil, fmt.Errorf("websocket: bad Sec-WebSocket-Accept header") | ||
| 280 | } | ||
| 281 | |||
| 282 | return &Conn{rwc: netConn, br: br, client: true}, nil | ||
| 283 | } | ||
| 284 | |||
| 210 | // Accept performs the server-side WebSocket handshake, hijacking the HTTP | 285 | // Accept performs the server-side WebSocket handshake, hijacking the HTTP |
| 211 | // connection and returning a Conn ready for framed I/O. | 286 | // connection and returning a Conn ready for framed I/O. |
| 212 | func Accept(w http.ResponseWriter, r *http.Request) (*Conn, error) { | 287 | func Accept(w http.ResponseWriter, r *http.Request) (*Conn, error) { |
