aboutsummaryrefslogtreecommitdiffstats
path: root/relay/websocket/websocket.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-09 12:48:39 -0700
committerbndw <ben@bdw.to>2026-03-09 12:48:39 -0700
commit0bd392e076e36c80a152abe00cbcd0bc9efedd9c (patch)
tree2727cf8a030e675fa86082b56faf9fee4ddc45b1 /relay/websocket/websocket.go
parent2c1ec8f9e964c2e89eee900299db667d9a58db25 (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/websocket/websocket.go')
-rw-r--r--relay/websocket/websocket.go75
1 files changed, 75 insertions, 0 deletions
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.
214func 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.
212func Accept(w http.ResponseWriter, r *http.Request) (*Conn, error) { 287func Accept(w http.ResponseWriter, r *http.Request) (*Conn, error) {