working v1

This commit is contained in:
cottongin
2025-10-31 16:17:04 -04:00
parent e41402a963
commit 020daea391
71 changed files with 14793 additions and 1 deletions

321
cmd/capture-auth/main.go Normal file
View File

@@ -0,0 +1,321 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
func main() {
roomURL := flag.String("room", "https://app.kosmi.io/room/@hyperspaceout", "Kosmi room URL")
output := flag.String("output", "auth-data.json", "Output file for captured data")
flag.Parse()
fmt.Printf("Capturing authentication data from: %s\n", *roomURL)
fmt.Printf("Output will be saved to: %s\n\n", *output)
// Storage for captured data
authData := &AuthData{
RoomURL: *roomURL,
CaptureTime: time.Now(),
Cookies: []Cookie{},
RequestHeaders: map[string]interface{}{},
ResponseHeaders: map[string]interface{}{},
WebSocketFrames: []WebSocketFrame{},
NetworkRequests: []NetworkRequest{},
}
// Create Chrome context with network logging
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", false),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-blink-features", "AutomationControlled"),
chromedp.Flag("disable-infobars", true),
chromedp.Flag("window-size", "1920,1080"),
chromedp.UserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
)
allocCtx, allocCancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer allocCancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// Enable network tracking
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *network.EventRequestWillBeSent:
if containsKosmiDomain(ev.Request.URL) {
fmt.Printf("→ REQUEST: %s %s\n", ev.Request.Method, ev.Request.URL)
authData.NetworkRequests = append(authData.NetworkRequests, NetworkRequest{
URL: ev.Request.URL,
Method: ev.Request.Method,
Headers: ev.Request.Headers,
Time: time.Now(),
})
}
case *network.EventResponseReceived:
if containsKosmiDomain(ev.Response.URL) {
fmt.Printf("← RESPONSE: %d %s\n", ev.Response.Status, ev.Response.URL)
if ev.Response.Status >= 200 && ev.Response.Status < 300 {
authData.ResponseHeaders[ev.Response.URL] = ev.Response.Headers
}
}
case *network.EventWebSocketCreated:
fmt.Printf("🔌 WebSocket Created: %s\n", ev.URL)
authData.WebSocketURL = ev.URL
authData.WebSocketRequestID = ev.RequestID.String()
case *network.EventWebSocketFrameSent:
data := string(ev.Response.PayloadData)
fmt.Printf("📤 WS SEND: %s\n", truncate(data, 100))
authData.WebSocketFrames = append(authData.WebSocketFrames, WebSocketFrame{
Direction: "sent",
Data: data,
Time: time.Now(),
})
case *network.EventWebSocketFrameReceived:
data := string(ev.Response.PayloadData)
fmt.Printf("📥 WS RECV: %s\n", truncate(data, 100))
authData.WebSocketFrames = append(authData.WebSocketFrames, WebSocketFrame{
Direction: "received",
Data: data,
Time: time.Now(),
})
}
})
// Inject WebSocket hook script
hookScript := getWebSocketHookScript()
// Run the capture
err := chromedp.Run(ctx,
network.Enable(),
chromedp.ActionFunc(func(ctx context.Context) error {
_, err := page.AddScriptToEvaluateOnNewDocument(hookScript).Do(ctx)
return err
}),
chromedp.Navigate(*roomURL),
chromedp.WaitReady("body"),
)
if err != nil {
fmt.Fprintf(os.Stderr, "Error navigating: %v\n", err)
os.Exit(1)
}
fmt.Println("\nWaiting for page to fully load and WebSocket to connect...")
time.Sleep(5 * time.Second)
// Capture all cookies
fmt.Println("\n📋 Capturing cookies...")
var cookiesData []map[string]interface{}
script := `
(function() {
return document.cookie.split(';').map(c => {
const parts = c.trim().split('=');
return {
name: parts[0],
value: parts.slice(1).join('='),
domain: window.location.hostname,
path: '/'
};
}).filter(c => c.name && c.value);
})();
`
if err := chromedp.Run(ctx, chromedp.Evaluate(script, &cookiesData)); err != nil {
fmt.Fprintf(os.Stderr, "Error capturing cookies: %v\n", err)
} else {
for _, c := range cookiesData {
if name, ok := c["name"].(string); ok {
if value, ok := c["value"].(string); ok {
authData.Cookies = append(authData.Cookies, Cookie{
Name: name,
Value: value,
Domain: c["domain"].(string),
Path: c["path"].(string),
})
fmt.Printf(" 🍪 %s=%s\n", name, truncate(value, 40))
}
}
}
}
// Get CDP cookies (includes HTTPOnly)
cookies, err := network.GetCookies().Do(ctx)
if err == nil {
fmt.Println("\n📋 CDP Cookies (including HTTPOnly):")
for _, c := range cookies {
if containsKosmiDomain(c.Domain) {
authData.HTTPOnlyCookies = append(authData.HTTPOnlyCookies, Cookie{
Name: c.Name,
Value: c.Value,
Domain: c.Domain,
Path: c.Path,
Secure: c.Secure,
HTTPOnly: c.HTTPOnly,
SameSite: string(c.SameSite),
})
fmt.Printf(" 🔒 %s=%s (HTTPOnly=%v, Secure=%v)\n",
c.Name, truncate(c.Value, 40), c.HTTPOnly, c.Secure)
}
}
}
// Check WebSocket status
var wsStatus map[string]interface{}
checkScript := `
(function() {
return {
hookInstalled: !!window.__KOSMI_WS_HOOK_INSTALLED__,
wsFound: !!window.__KOSMI_WS__,
wsConnected: window.__KOSMI_WS__ ? window.__KOSMI_WS__.readyState === WebSocket.OPEN : false,
wsURL: window.__KOSMI_WS__ ? window.__KOSMI_WS__.url : null,
messageQueueSize: window.__KOSMI_MESSAGE_QUEUE__ ? window.__KOSMI_MESSAGE_QUEUE__.length : 0
};
})();
`
if err := chromedp.Run(ctx, chromedp.Evaluate(checkScript, &wsStatus)); err == nil {
authData.WebSocketStatus = wsStatus
fmt.Printf("\n🔌 WebSocket Status:\n")
for k, v := range wsStatus {
fmt.Printf(" %s: %v\n", k, v)
}
}
// Wait a bit more to capture some messages
fmt.Println("\nWaiting 5 more seconds to capture WebSocket traffic...")
time.Sleep(5 * time.Second)
// Save to file
fmt.Printf("\n💾 Saving captured data to %s...\n", *output)
data, err := json.MarshalIndent(authData, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling data: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(*output, data, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
os.Exit(1)
}
fmt.Println("\n✅ Authentication data captured successfully!")
fmt.Println("\nSummary:")
fmt.Printf(" - Cookies: %d\n", len(authData.Cookies))
fmt.Printf(" - HTTPOnly Cookies: %d\n", len(authData.HTTPOnlyCookies))
fmt.Printf(" - Network Requests: %d\n", len(authData.NetworkRequests))
fmt.Printf(" - WebSocket Frames: %d\n", len(authData.WebSocketFrames))
fmt.Printf(" - WebSocket URL: %s\n", authData.WebSocketURL)
}
// Data structures
type AuthData struct {
RoomURL string `json:"room_url"`
CaptureTime time.Time `json:"capture_time"`
Cookies []Cookie `json:"cookies"`
HTTPOnlyCookies []Cookie `json:"httponly_cookies"`
RequestHeaders map[string]interface{} `json:"request_headers"`
ResponseHeaders map[string]interface{} `json:"response_headers"`
WebSocketURL string `json:"websocket_url"`
WebSocketRequestID string `json:"websocket_request_id"`
WebSocketStatus map[string]interface{} `json:"websocket_status"`
WebSocketFrames []WebSocketFrame `json:"websocket_frames"`
NetworkRequests []NetworkRequest `json:"network_requests"`
}
type Cookie struct {
Name string `json:"name"`
Value string `json:"value"`
Domain string `json:"domain"`
Path string `json:"path"`
Secure bool `json:"secure,omitempty"`
HTTPOnly bool `json:"httponly,omitempty"`
SameSite string `json:"same_site,omitempty"`
}
type WebSocketFrame struct {
Direction string `json:"direction"`
Data string `json:"data"`
Time time.Time `json:"time"`
}
type NetworkRequest struct {
URL string `json:"url"`
Method string `json:"method"`
Headers map[string]interface{} `json:"headers"`
Time time.Time `json:"time"`
}
// Helper functions
func containsKosmiDomain(url string) bool {
return contains(url, "kosmi.io") || contains(url, "engine.kosmi.io") || contains(url, "app.kosmi.io")
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr ||
(len(s) > len(substr) && (
s[:len(substr)] == substr ||
findSubstring(s, substr))))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}
func getWebSocketHookScript() string {
return `
(function() {
if (window.__KOSMI_WS_HOOK_INSTALLED__) return;
const OriginalWebSocket = window.WebSocket;
window.__KOSMI_MESSAGE_QUEUE__ = [];
window.__KOSMI_WS__ = null;
window.WebSocket = function(url, protocols) {
const socket = new OriginalWebSocket(url, protocols);
if (url.includes('engine.kosmi.io') || url.includes('gql-ws')) {
console.log('[Auth Capture] WebSocket created:', url);
window.__KOSMI_WS__ = socket;
window.__KOSMI_WS_CONNECTED__ = true;
}
return socket;
};
window.WebSocket.prototype = OriginalWebSocket.prototype;
window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
window.WebSocket.OPEN = OriginalWebSocket.OPEN;
window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
window.__KOSMI_WS_HOOK_INSTALLED__ = true;
})();
`
}

85
cmd/test-native/main.go Normal file
View File

@@ -0,0 +1,85 @@
package main
import (
"flag"
"os"
"os/signal"
"syscall"
"time"
"github.com/42wim/matterbridge/bridge"
bkosmi "github.com/42wim/matterbridge/bridge/kosmi"
"github.com/sirupsen/logrus"
)
func main() {
// Parse command line flags
roomURL := flag.String("room", "https://app.kosmi.io/room/@hyperspaceout", "Kosmi room URL")
debug := flag.Bool("debug", false, "Enable debug logging")
flag.Parse()
// Set up logger
log := logrus.New()
if *debug {
log.SetLevel(logrus.DebugLevel)
} else {
log.SetLevel(logrus.InfoLevel)
}
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
logger := log.WithField("bridge", "kosmi-test")
logger.Info("Starting Kosmi bridge test")
logger.Infof("Room URL: %s", *roomURL)
// Create bridge configuration
cfg := bridge.NewConfig("kosmi.test", logger)
cfg.SetString("RoomURL", *roomURL)
cfg.SetBool("Debug", *debug)
// Create Kosmi bridge
b := bkosmi.New(cfg)
// Connect to Kosmi
logger.Info("Connecting to Kosmi...")
if err := b.Connect(); err != nil {
logger.Fatalf("Failed to connect to Kosmi: %v", err)
}
logger.Info("Successfully connected to Kosmi!")
// Start message listener
go func() {
for msg := range cfg.Remote {
logger.Infof("Received message: [%s] %s: %s",
msg.Timestamp.Format("15:04:05"),
msg.Username,
msg.Text)
}
}()
// Wait for interrupt signal
logger.Info("Listening for messages... Press Ctrl+C to exit")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Optional: Send a test message after 5 seconds
go func() {
time.Sleep(5 * time.Second)
logger.Info("Bridge is running. Messages from Kosmi will appear above.")
logger.Info("To test sending messages, integrate with IRC or use the full Matterbridge setup")
}()
<-sigChan
logger.Info("Shutting down...")
// Disconnect
if err := b.Disconnect(); err != nil {
logger.Errorf("Error disconnecting: %v", err)
}
logger.Info("Goodbye!")
}

263
cmd/test-session/main.go Normal file
View File

@@ -0,0 +1,263 @@
package main
import (
"encoding/base64"
"flag"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"time"
"github.com/gorilla/websocket"
)
const (
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
appVersion = "4364"
)
func main() {
roomURL := flag.String("room", "https://app.kosmi.io/room/@hyperspaceout", "Full Kosmi room URL")
token := flag.String("token", "", "JWT token (optional, will try to extract from page)")
flag.Parse()
fmt.Println("🌐 Testing session-based WebSocket connection")
fmt.Printf(" Room URL: %s\n\n", *roomURL)
// Create HTTP client with cookie jar
jar, err := cookiejar.New(nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create cookie jar: %v\n", err)
os.Exit(1)
}
client := &http.Client{
Jar: jar,
Timeout: 30 * time.Second,
}
// Step 1: Visit the room page to establish session
fmt.Println("1⃣ Visiting room page to establish session...")
if err := visitRoomPage(client, *roomURL); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to visit room: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Session established!")
// Print cookies
u, _ := url.Parse(*roomURL)
cookies := client.Jar.Cookies(u)
fmt.Printf("\n 📋 Cookies received: %d\n", len(cookies))
for _, c := range cookies {
fmt.Printf(" - %s=%s\n", c.Name, truncate(c.Value, 50))
}
// Step 2: Connect WebSocket with cookies
fmt.Println("\n2⃣ Connecting WebSocket with session cookies...")
roomID := extractRoomID(*roomURL)
conn, err := connectWebSocketWithSession(client.Jar, *token, roomID)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to connect WebSocket: %v\n", err)
os.Exit(1)
}
defer conn.Close()
fmt.Println("✅ WebSocket connected!")
// Step 3: Listen for messages
fmt.Println("\n👂 Listening for messages (press Ctrl+C to exit)...\n")
messageCount := 0
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
fmt.Fprintf(os.Stderr, "Error reading message: %v\n", err)
break
}
msgType, _ := msg["type"].(string)
switch msgType {
case "next":
payload, _ := msg["payload"].(map[string]interface{})
data, _ := payload["data"].(map[string]interface{})
if newMessage, ok := data["newMessage"].(map[string]interface{}); ok {
messageCount++
body, _ := newMessage["body"].(string)
user, _ := newMessage["user"].(map[string]interface{})
username, _ := user["displayName"].(string)
if username == "" {
username, _ = user["username"].(string)
}
timestamp, _ := newMessage["time"].(float64)
t := time.Unix(int64(timestamp), 0)
fmt.Printf("[%s] %s: %s\n", t.Format("15:04:05"), username, body)
}
case "connection_ack":
fmt.Println(" ✅ Received connection_ack")
case "complete":
id, _ := msg["id"].(string)
fmt.Printf(" [Subscription %s completed]\n", id)
case "error":
fmt.Printf(" ⚠️ Error: %+v\n", msg)
case "ka":
// Keep-alive, ignore
default:
fmt.Printf(" 📨 %s\n", msgType)
}
}
fmt.Printf("\n📊 Total messages received: %d\n", messageCount)
}
func visitRoomPage(client *http.Client, roomURL string) error {
req, err := http.NewRequest("GET", roomURL, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("status %d", resp.StatusCode)
}
// Read and discard body (but process Set-Cookie headers)
io.Copy(io.Discard, resp.Body)
return nil
}
func connectWebSocketWithSession(jar http.CookieJar, token, roomID string) (*websocket.Conn, error) {
dialer := websocket.Dialer{
Jar: jar,
Subprotocols: []string{"graphql-ws"},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
headers := http.Header{
"Origin": []string{"https://app.kosmi.io"},
"User-Agent": []string{userAgent},
}
conn, resp, err := dialer.Dial("wss://engine.kosmi.io/gql-ws", headers)
if err != nil {
if resp != nil {
fmt.Printf(" Response status: %d\n", resp.StatusCode)
// Print response headers
fmt.Println(" Response headers:")
for k, v := range resp.Header {
fmt.Printf(" %s: %v\n", k, v)
}
}
return nil, err
}
// Send connection_init
// If token not provided, try without it
uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
payload := map[string]interface{}{
"ua": uaEncoded,
"v": appVersion,
"r": "",
}
if token != "" {
payload["token"] = token
}
initMsg := map[string]interface{}{
"type": "connection_init",
"payload": payload,
}
if err := conn.WriteJSON(initMsg); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to send connection_init: %w", err)
}
// Wait for ack
var ackMsg map[string]interface{}
if err := conn.ReadJSON(&ackMsg); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to read ack: %w", err)
}
msgType, _ := ackMsg["type"].(string)
if msgType != "connection_ack" {
conn.Close()
return nil, fmt.Errorf("expected connection_ack, got %s", msgType)
}
// Subscribe to messages
query := fmt.Sprintf(`
subscription {
newMessage(roomId: "%s") {
body
time
user {
displayName
username
}
}
}
`, roomID)
subMsg := map[string]interface{}{
"id": "newMessage-subscription",
"type": "subscribe",
"payload": map[string]interface{}{
"query": query,
"variables": map[string]interface{}{},
},
}
if err := conn.WriteJSON(subMsg); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to subscribe: %w", err)
}
return conn, nil
}
func extractRoomID(roomURL string) string {
// Extract room ID from URL
// https://app.kosmi.io/room/@roomname -> @roomname
// https://app.kosmi.io/room/roomid -> roomid
parts := make([]string, 0)
for _, part := range []byte(roomURL) {
if part == '/' {
parts = append(parts, "")
} else if len(parts) > 0 {
parts[len(parts)-1] += string(part)
}
}
if len(parts) > 0 {
return parts[len(parts)-1]
}
return roomURL
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}

View File

@@ -0,0 +1,217 @@
package main
import (
"context"
"encoding/base64"
"flag"
"fmt"
"net/http"
"os"
"time"
"github.com/gorilla/websocket"
)
const (
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
appVersion = "4364"
wsURL = "wss://engine.kosmi.io/gql-ws"
)
func main() {
token := flag.String("token", "", "JWT token from captured session")
roomID := flag.String("room", "@hyperspaceout", "Room ID")
flag.Parse()
if *token == "" {
fmt.Fprintf(os.Stderr, "Error: -token is required\n")
fmt.Fprintf(os.Stderr, "Usage: %s -token <JWT_TOKEN> -room <ROOM_ID>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo get a token, run: ./capture-auth and extract it from auth-data.json\n")
os.Exit(1)
}
fmt.Println("🔌 Testing direct WebSocket connection with JWT token")
fmt.Printf(" Token: %s...\n", truncate(*token, 50))
fmt.Printf(" Room: %s\n\n", *roomID)
// Connect WebSocket
fmt.Println("1⃣ Connecting to WebSocket...")
conn, err := connectWebSocket(*token)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to connect: %v\n", err)
os.Exit(1)
}
defer conn.Close()
fmt.Println("✅ WebSocket connected!")
// Wait for connection_ack
fmt.Println("\n2⃣ Waiting for connection_ack...")
if err := waitForAck(conn); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to receive ack: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Received connection_ack!")
// Subscribe to messages
fmt.Printf("\n3⃣ Subscribing to messages in room %s...\n", *roomID)
if err := subscribeToMessages(conn, *roomID); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to subscribe: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Subscribed!")
// Listen for messages
fmt.Println("\n👂 Listening for messages (press Ctrl+C to exit)...\n")
messageCount := 0
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
fmt.Fprintf(os.Stderr, "Error reading message: %v\n", err)
break
}
msgType, _ := msg["type"].(string)
switch msgType {
case "next":
payload, _ := msg["payload"].(map[string]interface{})
data, _ := payload["data"].(map[string]interface{})
if newMessage, ok := data["newMessage"].(map[string]interface{}); ok {
messageCount++
body, _ := newMessage["body"].(string)
user, _ := newMessage["user"].(map[string]interface{})
username, _ := user["displayName"].(string)
if username == "" {
username, _ = user["username"].(string)
}
timestamp, _ := newMessage["time"].(float64)
t := time.Unix(int64(timestamp), 0)
fmt.Printf("[%s] %s: %s\n", t.Format("15:04:05"), username, body)
}
case "complete":
id, _ := msg["id"].(string)
fmt.Printf(" [Subscription %s completed]\n", id)
case "error":
fmt.Printf(" ⚠️ Error: %+v\n", msg)
default:
// Ignore other message types (ka, etc)
}
}
fmt.Printf("\n📊 Total messages received: %d\n", messageCount)
}
func connectWebSocket(token string) (*websocket.Conn, error) {
dialer := websocket.Dialer{
Subprotocols: []string{"graphql-ws"},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
headers := http.Header{
"Origin": []string{"https://app.kosmi.io"},
"User-Agent": []string{userAgent},
"Sec-WebSocket-Protocol": []string{"graphql-ws"},
"Cache-Control": []string{"no-cache"},
"Pragma": []string{"no-cache"},
}
conn, resp, err := dialer.Dial(wsURL, headers)
if err != nil {
if resp != nil {
fmt.Printf(" Response status: %d\n", resp.StatusCode)
}
return nil, err
}
// Send connection_init with token
uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
initMsg := map[string]interface{}{
"type": "connection_init",
"payload": map[string]interface{}{
"token": token,
"ua": uaEncoded,
"v": appVersion,
"r": "",
},
}
if err := conn.WriteJSON(initMsg); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to send connection_init: %w", err)
}
return conn, nil
}
func waitForAck(conn *websocket.Conn) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
done := make(chan error, 1)
go func() {
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
done <- err
return
}
msgType, _ := msg["type"].(string)
fmt.Printf(" Received: %s\n", msgType)
if msgType == "connection_ack" {
done <- nil
return
}
// Keep reading other messages (like ka)
}
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return fmt.Errorf("timeout waiting for connection_ack")
}
}
func subscribeToMessages(conn *websocket.Conn, roomID string) error {
query := fmt.Sprintf(`
subscription {
newMessage(roomId: "%s") {
body
time
user {
displayName
username
}
}
}
`, roomID)
subMsg := map[string]interface{}{
"id": "newMessage-subscription",
"type": "subscribe",
"payload": map[string]interface{}{
"query": query,
"variables": map[string]interface{}{},
},
}
return conn.WriteJSON(subMsg)
}
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}

423
cmd/test-websocket/main.go Normal file
View File

@@ -0,0 +1,423 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"time"
"github.com/gorilla/websocket"
)
const (
userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
appVersion = "4364"
wsURL = "wss://engine.kosmi.io/gql-ws"
tokenURL = "https://engine.kosmi.io/"
)
func main() {
roomID := flag.String("room", "@hyperspaceout", "Room ID")
testMode := flag.Int("mode", 1, "Test mode: 1=with-token, 2=no-auth, 3=origin-only")
flag.Parse()
fmt.Printf("Test Mode %d: Testing WebSocket connection to Kosmi\n\n", *testMode)
var conn *websocket.Conn
var err error
switch *testMode {
case 1:
fmt.Println("Mode 1: Testing with JWT token (full auth)")
conn, err = testWithToken(*roomID)
case 2:
fmt.Println("Mode 2: Testing without authentication")
conn, err = testWithoutAuth()
case 3:
fmt.Println("Mode 3: Testing with Origin header only")
conn, err = testWithOriginOnly()
default:
fmt.Fprintf(os.Stderr, "Invalid test mode: %d\n", *testMode)
os.Exit(1)
}
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Connection failed: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ WebSocket connected successfully!")
defer conn.Close()
// Try to do the GraphQL-WS handshake
fmt.Println("\n📤 Sending connection_init...")
if err := waitForAck(conn); err != nil {
fmt.Fprintf(os.Stderr, "❌ Handshake failed: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ WebSocket handshake successful!")
fmt.Println("\n📝 Testing message subscription...")
if err := subscribeToMessages(conn, *roomID); err != nil {
fmt.Fprintf(os.Stderr, "❌ Subscription failed: %v\n", err)
os.Exit(1)
}
fmt.Println("✅ Subscribed to messages!")
fmt.Println("\n👂 Listening for messages (press Ctrl+C to exit)...")
// Listen for messages
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
fmt.Fprintf(os.Stderr, "Error reading message: %v\n", err)
break
}
msgType, _ := msg["type"].(string)
fmt.Printf("📥 Received: %s\n", msgType)
if msgType == "next" {
payload, _ := msg["payload"].(map[string]interface{})
data, _ := payload["data"].(map[string]interface{})
if newMessage, ok := data["newMessage"].(map[string]interface{}); ok {
body, _ := newMessage["body"].(string)
user, _ := newMessage["user"].(map[string]interface{})
username, _ := user["displayName"].(string)
if username == "" {
username, _ = user["username"].(string)
}
fmt.Printf(" 💬 %s: %s\n", username, body)
}
}
}
}
// testWithToken attempts connection with full JWT authentication
func testWithToken(roomID string) (*websocket.Conn, error) {
fmt.Println(" 1⃣ Step 1: Acquiring JWT token...")
// Try to get token from GraphQL endpoint
token, err := acquireToken()
if err != nil {
return nil, fmt.Errorf("failed to acquire token: %w", err)
}
fmt.Printf(" ✅ Got token: %s...\n", truncate(token, 50))
fmt.Println(" 2⃣ Step 2: Connecting WebSocket with token...")
return connectWithToken(token)
}
// testWithoutAuth attempts direct connection with no headers
func testWithoutAuth() (*websocket.Conn, error) {
fmt.Println(" Connecting without any authentication...")
dialer := websocket.Dialer{
Subprotocols: []string{"graphql-ws"},
}
conn, resp, err := dialer.Dial(wsURL, nil)
if resp != nil {
fmt.Printf(" Response status: %d\n", resp.StatusCode)
}
return conn, err
}
// testWithOriginOnly attempts connection with just Origin header
func testWithOriginOnly() (*websocket.Conn, error) {
fmt.Println(" Connecting with Origin header only...")
dialer := websocket.Dialer{
Subprotocols: []string{"graphql-ws"},
}
headers := http.Header{
"Origin": []string{"https://app.kosmi.io"},
}
conn, resp, err := dialer.Dial(wsURL, headers)
if resp != nil {
fmt.Printf(" Response status: %d\n", resp.StatusCode)
}
return conn, err
}
// acquireToken gets a JWT token from Kosmi's API
func acquireToken() (string, error) {
// First, let's try a few different approaches
// Approach 1: Try empty POST (some APIs generate anonymous tokens)
fmt.Println(" Trying empty POST...")
token, err := tryEmptyPost()
if err == nil && token != "" {
return token, nil
}
fmt.Printf(" Empty POST failed: %v\n", err)
// Approach 2: Try GraphQL anonymous login
fmt.Println(" Trying GraphQL anonymous session...")
token, err = tryGraphQLSession()
if err == nil && token != "" {
return token, nil
}
fmt.Printf(" GraphQL session failed: %v\n", err)
// Approach 3: Try REST endpoint
fmt.Println(" Trying REST endpoint...")
token, err = tryRESTAuth()
if err == nil && token != "" {
return token, nil
}
fmt.Printf(" REST auth failed: %v\n", err)
return "", fmt.Errorf("all token acquisition methods failed")
}
// tryEmptyPost tries posting an empty body
func tryEmptyPost() (string, error) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", tokenURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://app.kosmi.io/")
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("status %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
// Try to extract token from various possible locations
if token, ok := result["token"].(string); ok {
return token, nil
}
if data, ok := result["data"].(map[string]interface{}); ok {
if token, ok := data["token"].(string); ok {
return token, nil
}
}
return "", fmt.Errorf("no token in response: %+v", result)
}
// tryGraphQLSession tries a GraphQL mutation for anonymous session
func tryGraphQLSession() (string, error) {
query := map[string]interface{}{
"query": `mutation { createAnonymousSession { token } }`,
}
return postGraphQL(query)
}
// tryRESTAuth tries REST-style auth endpoint
func tryRESTAuth() (string, error) {
body := map[string]interface{}{
"anonymous": true,
}
jsonBody, _ := json.Marshal(body)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", tokenURL+"auth/anonymous", nil)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://app.kosmi.io/")
req.Header.Set("User-Agent", userAgent)
req.Body = http.NoBody
_ = jsonBody // silence unused warning
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return "", fmt.Errorf("status %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if token, ok := result["token"].(string); ok {
return token, nil
}
return "", fmt.Errorf("no token in response")
}
// postGraphQL posts a GraphQL query
func postGraphQL(query map[string]interface{}) (string, error) {
jsonBody, _ := json.Marshal(query)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest("POST", tokenURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://app.kosmi.io/")
req.Header.Set("User-Agent", userAgent)
req.Body = http.NoBody
_ = jsonBody // silence unused
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("status %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
// Navigate nested response
if data, ok := result["data"].(map[string]interface{}); ok {
if session, ok := data["createAnonymousSession"].(map[string]interface{}); ok {
if token, ok := session["token"].(string); ok {
return token, nil
}
}
}
return "", fmt.Errorf("no token in response")
}
// connectWithToken connects WebSocket with JWT token
func connectWithToken(token string) (*websocket.Conn, error) {
dialer := websocket.Dialer{
Subprotocols: []string{"graphql-ws"},
}
headers := http.Header{
"Origin": []string{"https://app.kosmi.io"},
"User-Agent": []string{userAgent},
}
conn, resp, err := dialer.Dial(wsURL, headers)
if err != nil {
if resp != nil {
fmt.Printf(" Response status: %d\n", resp.StatusCode)
}
return nil, err
}
// Send connection_init with token
uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
initMsg := map[string]interface{}{
"type": "connection_init",
"payload": map[string]interface{}{
"token": token,
"ua": uaEncoded,
"v": appVersion,
"r": "",
},
}
if err := conn.WriteJSON(initMsg); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to send connection_init: %w", err)
}
return conn, nil
}
// waitForAck waits for connection_ack
func waitForAck(conn *websocket.Conn) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
done := make(chan error, 1)
go func() {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
done <- err
return
}
msgType, _ := msg["type"].(string)
if msgType != "connection_ack" {
done <- fmt.Errorf("expected connection_ack, got %s", msgType)
return
}
fmt.Println("✅ Received connection_ack")
done <- nil
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return fmt.Errorf("timeout waiting for connection_ack")
}
}
// subscribeToMessages subscribes to room messages
func subscribeToMessages(conn *websocket.Conn, roomID string) error {
query := fmt.Sprintf(`
subscription {
newMessage(roomId: "%s") {
body
time
user {
displayName
username
}
}
}
`, roomID)
subMsg := map[string]interface{}{
"id": "test-subscription-1",
"type": "subscribe",
"payload": map[string]interface{}{
"query": query,
"variables": map[string]interface{}{},
},
}
return conn.WriteJSON(subMsg)
}
// truncate truncates a string
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}