working v1
This commit is contained in:
423
cmd/test-websocket/main.go
Normal file
423
cmd/test-websocket/main.go
Normal 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] + "..."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user