ysweet_proxy.go
1package rtw
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8
9 "git.kilimanjaro.io/rtw/api"
10 "git.kilimanjaro.io/rtw/pkg/log"
11 "git.kilimanjaro.io/rtw/user"
12 "git.kilimanjaro.io/ygo/ysweet"
13 "github.com/coder/websocket"
14)
15
16// YSweetProxyHandler returns an HTTP handler that proxies websocket connections
17// to a y-sweet server with logging hooks for connect, upgrade, and disconnect events.
18// The docManager parameter is optional - if nil, no document management is performed.
19// Additional ysweet.ProxyOption parameters can be passed to configure the proxy
20// (e.g., WithAllowedOrigins, WithTargetAuthToken, etc.).
21func YSweetProxyHandler(targetURL string, docManager *DocManager, us *user.Service, extraOpts ...ysweet.ProxyOption) http.Handler {
22 opts := []ysweet.ProxyOption{
23 ysweet.WithDocIDFunc(func(r *http.Request) string {
24 return r.PathValue("docID")
25 }),
26 ysweet.WithOnConnect(func(ctx context.Context, docID string, w http.ResponseWriter, r *http.Request) error {
27 l := log.FromContext(r.Context())
28 token := r.URL.Query().Get("token")
29 if token == "" {
30 l.Error("unexpected error: no token on websocket request")
31 return fmt.Errorf("expected token")
32 }
33 validToken, err := api.ValidateDocumentToken(token, docID)
34 if err != nil {
35 l.Error("unexpected error: token failed validation", "error", err)
36 return fmt.Errorf("token failed validation: %w", err)
37 }
38 userID, _ := validToken.GetString("user_id")
39 slog.Info("websocket connect", "doc_id", docID, "user_id", userID, "remote_addr", r.RemoteAddr)
40 return nil
41 }),
42 ysweet.WithOnWebsocketUpgrade(func(ctx context.Context, docID string, clientConn, upstreamConn *websocket.Conn) error {
43 slog.Info("websocket upgrade", "doc_id", docID)
44 // Notify DocManager of client connection
45 if docManager != nil {
46 docManager.ClientConnect(docID)
47 }
48 return nil
49 }),
50 ysweet.WithOnDisconnect(func(ctx context.Context, docID string, reason ysweet.DisconnectReason) error {
51 reasonStr := "unknown"
52 switch reason {
53 case ysweet.DisconnectReasonClientClosed:
54 reasonStr = "client_closed"
55 case ysweet.DisconnectReasonUpstreamClosed:
56 reasonStr = "upstream_closed"
57 case ysweet.DisconnectReasonError:
58 reasonStr = "error"
59 case ysweet.DisconnectReasonContextCancelled:
60 reasonStr = "context_cancelled"
61 }
62 slog.Info("websocket disconnect", "doc_id", docID, "reason", reasonStr)
63 // Notify DocManager of client disconnect
64 if docManager != nil {
65 docManager.ClientDisconnect(docID)
66 }
67 return nil
68 }),
69 }
70
71 // Append any extra options passed by the caller
72 opts = append(opts, extraOpts...)
73
74 return ysweet.ProxyHandler(targetURL, opts...)
75}