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:
commit
ef3c2cd1b0
4 changed files with 114 additions and 3 deletions
60
examples/ws_connect.go
Normal file
60
examples/ws_connect.go
Normal 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.")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue