Merge pull request #1 from nglmercer/fix-websocket-auth-windows-pico-ws-token-header-casing-9287435458302215249

Fix WebSocket authentication and connection issues on Windows
This commit is contained in:
melserngl 2026-04-05 09:51:53 -05:00 committed by GitHub
commit ef3c2cd1b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 114 additions and 3 deletions

60
examples/ws_connect.go Normal file
View file

@ -0,0 +1,60 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/gorilla/websocket"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: go run examples/ws_connect.go <ws_url> <token>")
fmt.Println("Example: go run examples/ws_connect.go ws://localhost:18800/pico/ws my-secret-token")
return
}
wsURL := os.Args[1]
token := os.Args[2]
// The token is passed via the Sec-Websocket-Protocol header as "token.<value>".
// This is the standard way to pass an auth token for WebSockets in a browser-compatible way.
header := http.Header{}
header.Set("Sec-Websocket-Protocol", "token."+token)
fmt.Printf("Connecting to %s with token...\n", wsURL)
dialer := websocket.DefaultDialer
conn, resp, err := dialer.Dial(wsURL, header)
if err != nil {
if resp != nil {
log.Fatalf("Handshake failed with status %d: %v", resp.StatusCode, err)
}
log.Fatalf("Connection failed: %v", err)
}
defer conn.Close()
// On success, the server echoes the subprotocol back.
fmt.Printf("Connected successfully! Server subprotocol: %s\n", conn.Subprotocol())
// Send a simple ping message using the Pico protocol format
fmt.Println("Sending ping message...")
pingMsg := map[string]any{
"type": "ping",
"id": "msg-1",
}
if err := conn.WriteJSON(pingMsg); err != nil {
log.Fatalf("Failed to send ping: %v", err)
}
// Read response
fmt.Println("Waiting for pong...")
var pongMsg map[string]any
if err := conn.ReadJSON(&pongMsg); err != nil {
log.Fatalf("Failed to read pong: %v", err)
}
fmt.Printf("Received response: %+v\n", pongMsg)
fmt.Println("Test completed successfully.")
}

View file

@ -349,7 +349,7 @@ func (c *PicoChannel) handleWebSocket(w http.ResponseWriter, r *http.Request) {
// Echo the matched subprotocol back so the browser accepts the upgrade.
var responseHeader http.Header
if proto := c.matchedSubprotocol(r); proto != "" {
responseHeader = http.Header{"Sec-WebSocket-Protocol": {proto}}
responseHeader = http.Header{"Sec-Websocket-Protocol": {proto}}
}
conn, err := c.upgrader.Upgrade(w, r, responseHeader)
@ -387,7 +387,7 @@ func (c *PicoChannel) handleWebSocket(w http.ResponseWriter, r *http.Request) {
// authenticate checks the request for a valid token:
// 1. Authorization: Bearer <token> header
// 2. Sec-WebSocket-Protocol "token.<value>" (for browsers that can't set headers)
// 2. Sec-Websocket-Protocol "token.<value>" (for browsers that can't set headers)
// 3. Query parameter "token" (only when AllowTokenQuery is on)
func (c *PicoChannel) authenticate(r *http.Request) bool {
token := c.config.Token.String()
@ -403,7 +403,7 @@ func (c *PicoChannel) authenticate(r *http.Request) bool {
}
}
// Check Sec-WebSocket-Protocol subprotocol ("token.<value>")
// Check Sec-Websocket-Protocol subprotocol ("token.<value>")
if c.matchedSubprotocol(r) != "" {
return true
}

View file

@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"testing"
@ -142,3 +144,49 @@ func (c *PicoChannel) addConnForTest(pc *picoConn) {
}
bySession[pc.id] = pc
}
func TestPicoChannel_AuthenticateSubprotocol(t *testing.T) {
cfg := config.PicoConfig{}
cfg.SetToken("secret-token")
ch, _ := NewPicoChannel(cfg, bus.NewMessageBus())
tests := []struct {
name string
subprotocols []string
want bool
}{
{
name: "Correct subprotocol",
subprotocols: []string{"token.secret-token"},
want: true,
},
{
name: "Incorrect subprotocol",
subprotocols: []string{"token.wrong-token"},
want: false,
},
{
name: "Multiple subprotocols with correct one",
subprotocols: []string{"other-proto", "token.secret-token"},
want: true,
},
{
name: "No subprotocols",
subprotocols: []string{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
header := make(http.Header)
if len(tt.subprotocols) > 0 {
header.Set("Sec-Websocket-Protocol", strings.Join(tt.subprotocols, ", "))
}
req := &http.Request{Header: header}
if got := ch.authenticate(req); got != tt.want {
t.Errorf("authenticate() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -166,6 +166,9 @@ func isPublicLauncherDashboardPath(method, p string) bool {
if isPublicLauncherDashboardStatic(method, p) {
return true
}
if p == "/pico/ws" && method == http.MethodGet {
return true
}
switch p {
case "/api/auth/login":
return method == http.MethodPost