proxy_server.go

  1//go:build ignore
  2// +build ignore
  3
  4// This example demonstrates how to use the ysweet proxy handler to create
  5// a websocket proxy server that forwards connections to a y-sweet server.
  6//
  7// Usage:
  8//
  9//	go run proxy_server.go
 10//
 11// Then connect from a Yjs client to ws://localhost:3000/d/my-doc/ws/my-doc
 12package main
 13
 14import (
 15	"context"
 16	"log"
 17	"net/http"
 18	"os"
 19	"os/signal"
 20	"syscall"
 21	"time"
 22
 23	"git.kilimanjaro.io/ygo/ysweet"
 24	"github.com/coder/websocket"
 25)
 26
 27func main() {
 28	// Configuration
 29	upstreamURL := getEnv("Y_SWEET_URL", "ws://localhost:8080")
 30	listenAddr := getEnv("LISTEN_ADDR", ":3000")
 31	authToken := getEnv("Y_SWEET_AUTH_TOKEN", "")
 32
 33	// Create a master context for graceful shutdown
 34	// When this context is cancelled, all active websocket connections
 35	// will be closed with a 5-second drain period
 36	masterCtx, cancel := context.WithCancel(context.Background())
 37	defer cancel()
 38
 39	// Example 1: Use default URL pattern (/d/{docID}/ws/{docID})
 40	defaultHandler := ysweet.ProxyHandler(upstreamURL,
 41		ysweet.WithContext(masterCtx), // Enable graceful shutdown
 42		ysweet.WithTargetAuthToken(authToken),
 43		ysweet.WithOnConnect(func(ctx context.Context, docID string, w http.ResponseWriter, r *http.Request) error {
 44			log.Printf("[CONNECT] Client %s connecting to doc %s", r.RemoteAddr, docID)
 45			// You can now take action based on docID, e.g., check permissions
 46			return nil
 47		}),
 48		ysweet.WithOnWebsocketUpgrade(func(ctx context.Context, docID string, clientConn, upstreamConn *websocket.Conn) error {
 49			log.Printf("[UPGRADE] Doc %s websocket upgrade successful", docID)
 50			return nil
 51		}),
 52		ysweet.WithOnDisconnect(func(ctx context.Context, docID string, reason ysweet.DisconnectReason) error {
 53			log.Printf("[DISCONNECT] Doc %s disconnected, reason: %v", docID, reason)
 54			return nil
 55		}),
 56	)
 57
 58	// Example 2: Custom URL pattern using Go 1.22+ path parameters
 59	customHandler := ysweet.ProxyHandler(upstreamURL,
 60		ysweet.WithContext(masterCtx), // Enable graceful shutdown
 61		ysweet.WithDocIDFunc(func(r *http.Request) string {
 62			// Extract docID from custom URL: /ws/{docID}
 63			return r.PathValue("docID")
 64		}),
 65		ysweet.WithOnConnect(func(ctx context.Context, docID string, w http.ResponseWriter, r *http.Request) error {
 66			log.Printf("[CONNECT] Custom handler - doc %s connecting", docID)
 67			return nil
 68		}),
 69	)
 70
 71	// Set up HTTP routes
 72	mux := http.NewServeMux()
 73
 74	// Default y-sweet pattern
 75	mux.Handle("/d/", defaultHandler)
 76
 77	// Custom pattern: /ws/{docID} -> proxies to upstream /d/{docID}/ws/{docID}
 78	mux.Handle("/ws/{docID}", customHandler)
 79
 80	// Health check endpoint
 81	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
 82		w.WriteHeader(http.StatusOK)
 83		w.Write([]byte("ok"))
 84	})
 85
 86	// Create server
 87	server := &http.Server{
 88		Addr:    listenAddr,
 89		Handler: mux,
 90	}
 91
 92	// Handle graceful shutdown
 93	sigChan := make(chan os.Signal, 1)
 94	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
 95
 96	go func() {
 97		<-sigChan
 98		log.Println("Shutting down - closing all websocket connections...")
 99
100		// Cancel master context first to close all websocket connections
101		// The proxy will wait 5 seconds for connections to drain to upstream
102		cancel()
103
104		// Then shutdown the HTTP server
105		shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
106		defer shutdownCancel()
107
108		if err := server.Shutdown(shutdownCtx); err != nil {
109			log.Printf("Server shutdown error: %v", err)
110		}
111	}()
112
113	log.Printf("Starting proxy server on %s", listenAddr)
114	log.Printf("Proxying to y-sweet at %s", upstreamURL)
115
116	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
117		log.Fatalf("Server error: %v", err)
118	}
119}
120
121func getEnv(key, defaultValue string) string {
122	if value := os.Getenv(key); value != "" {
123		return value
124	}
125	return defaultValue
126}