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}