mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix: proxy WebSocket through web server port (#1665)
- Modify buildWsURL to use web server port (18800) instead of gateway port (18790) - Add WebSocket proxy handler to forward /pico/ws to gateway - Gateway port is read from config (cfg.Gateway.Port), defaults to 18790 - This allows WebSocket connections through the same port as the web UI, avoiding the need to expose extra ports for Tailscale/Docker
This commit is contained in:
@@ -80,5 +80,11 @@ func (h *Handler) buildWsURL(r *http.Request, cfg *config.Config) string {
|
||||
if host == "" || host == "0.0.0.0" {
|
||||
host = requestHostName(r)
|
||||
}
|
||||
return requestWSScheme(r) + "://" + net.JoinHostPort(host, strconv.Itoa(cfg.Gateway.Port)) + "/pico/ws"
|
||||
// Use web server port instead of gateway port to avoid exposing extra ports
|
||||
// The WebSocket connection will be proxied by the backend to the gateway
|
||||
wsPort := h.serverPort
|
||||
if wsPort == 0 {
|
||||
wsPort = 18800 // default web server port
|
||||
}
|
||||
return requestWSScheme(r) + "://" + net.JoinHostPort(host, strconv.Itoa(wsPort)) + "/pico/ws"
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ func TestBuildWsURLUsesRequestHostWhenLauncherPublicSaved(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://launcher.local/api/pico/token", nil)
|
||||
req.Host = "192.168.1.9:18800"
|
||||
|
||||
if got := h.buildWsURL(req, cfg); got != "ws://192.168.1.9:18790/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "ws://192.168.1.9:18790/pico/ws")
|
||||
if got := h.buildWsURL(req, cfg); got != "ws://192.168.1.9:18800/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "ws://192.168.1.9:18800/pico/ws")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ func TestBuildWsURLUsesWSSWhenForwardedProtoIsHTTPS(t *testing.T) {
|
||||
req.Host = "chat.example.com"
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
|
||||
if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com:18790/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com:18790/pico/ws")
|
||||
if got := h.buildWsURL(req, cfg); got != "wss://chat.example.com:18800/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "wss://chat.example.com:18800/pico/ws")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ func TestBuildWsURLUsesWSSWhenRequestIsTLS(t *testing.T) {
|
||||
req.Host = "secure.example.com"
|
||||
req.TLS = &tls.ConnectionState{}
|
||||
|
||||
if got := h.buildWsURL(req, cfg); got != "wss://secure.example.com:18790/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "wss://secure.example.com:18790/pico/ws")
|
||||
if got := h.buildWsURL(req, cfg); got != "wss://secure.example.com:18800/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "wss://secure.example.com:18800/pico/ws")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ func TestBuildWsURLPrefersForwardedHTTPOverTLS(t *testing.T) {
|
||||
req.TLS = &tls.ConnectionState{}
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
|
||||
if got := h.buildWsURL(req, cfg); got != "ws://chat.example.com:18790/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "ws://chat.example.com:18790/pico/ws")
|
||||
if got := h.buildWsURL(req, cfg); got != "ws://chat.example.com:18800/pico/ws" {
|
||||
t.Fatalf("buildWsURL() = %q, want %q", got, "ws://chat.example.com:18800/pico/ws")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
@@ -16,6 +18,39 @@ func (h *Handler) registerPicoRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/pico/token", h.handleGetPicoToken)
|
||||
mux.HandleFunc("POST /api/pico/token", h.handleRegenPicoToken)
|
||||
mux.HandleFunc("POST /api/pico/setup", h.handlePicoSetup)
|
||||
|
||||
// WebSocket proxy: forward /pico/ws to gateway
|
||||
// This allows the frontend to connect via the same port as the web UI,
|
||||
// avoiding the need to expose extra ports for WebSocket communication.
|
||||
wsProxy := h.createWsProxy()
|
||||
mux.HandleFunc("GET /pico/ws", h.handleWebSocketProxy(wsProxy))
|
||||
}
|
||||
|
||||
// createWsProxy creates a reverse proxy to the gateway WebSocket endpoint.
|
||||
// The gateway port is read from the configuration.
|
||||
func (h *Handler) createWsProxy() *httputil.ReverseProxy {
|
||||
cfg, err := config.LoadConfig(h.configPath)
|
||||
gatewayPort := 18790 // default
|
||||
if err == nil && cfg.Gateway.Port != 0 {
|
||||
gatewayPort = cfg.Gateway.Port
|
||||
}
|
||||
gatewayURL, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", gatewayPort))
|
||||
wsProxy := httputil.NewSingleHostReverseProxy(gatewayURL)
|
||||
wsProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
http.Error(w, "Gateway unavailable: "+err.Error(), http.StatusBadGateway)
|
||||
}
|
||||
return wsProxy
|
||||
}
|
||||
|
||||
// handleWebSocketProxy wraps a reverse proxy to handle WebSocket connections.
|
||||
// It ensures the Connection and Upgrade headers are properly forwarded.
|
||||
func (h *Handler) handleWebSocketProxy(proxy *httputil.ReverseProxy) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set headers for WebSocket upgrade
|
||||
r.Header.Set("Connection", "upgrade")
|
||||
r.Header.Set("Upgrade", "websocket")
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetPicoToken returns the current WS token and URL for the frontend.
|
||||
|
||||
Reference in New Issue
Block a user