package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/signal" "time" "github.com/gorilla/websocket" ) const ( // Kosmi WebSocket endpoint kosmiWSURL = "wss://engine.kosmi.io/gql-ws" // Your room ID (MUST include @ symbol!) roomID = "@hyperspaceout" // User agent and app version (from AUTH_FINDINGS.md) 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" ) // GraphQL-WS Protocol message types const ( MessageTypeConnectionInit = "connection_init" MessageTypeConnectionAck = "connection_ack" MessageTypeSubscribe = "subscribe" MessageTypeNext = "next" MessageTypeError = "error" MessageTypeComplete = "complete" ) // Message represents a graphql-ws protocol message type Message struct { ID string `json:"id,omitempty"` Type string `json:"type"` Payload interface{} `json:"payload,omitempty"` // Can be map or array } func main() { log.Println("šŸš€ Starting Kosmi GraphQL-WS Proof of Concept") log.Printf("šŸ“” WebSocket URL: %s", kosmiWSURL) log.Printf("šŸ  Room ID: %s", roomID) // Step 1: Get anonymous token log.Println("\nšŸ“ Step 1: Getting anonymous token...") token, err := getAnonymousToken() if err != nil { log.Fatalf("āŒ Failed to get token: %v", err) } log.Printf("āœ… Got token: %s...", token[:50]) // Set up interrupt handler interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) log.Println("\nšŸ”Œ Step 2: Connecting to WebSocket...") // Create WebSocket dialer with headers dialer := websocket.Dialer{ Subprotocols: []string{"graphql-transport-ws"}, HandshakeTimeout: 10 * time.Second, } // Add headers that a browser would send headers := http.Header{} headers.Add("Origin", "https://app.kosmi.io") headers.Add("User-Agent", userAgent) // Connect to WebSocket log.Println("šŸ”Œ Establishing WebSocket connection...") conn, resp, err := dialer.Dial(kosmiWSURL, headers) if err != nil { log.Fatalf("āŒ Failed to connect: %v (response: %+v)", err, resp) } defer conn.Close() log.Println("āœ… WebSocket connected!") log.Printf("šŸ“‹ Response status: %s", resp.Status) log.Printf("šŸ“‹ Subprotocol: %s", conn.Subprotocol()) // Channel for receiving messages done := make(chan struct{}) // Start reading messages go func() { defer close(done) for { var msg Message err := conn.ReadJSON(&msg) if err != nil { log.Printf("āŒ Read error: %v", err) return } log.Printf("šŸ“Ø Received: %s (id: %s)", msg.Type, msg.ID) if msg.Payload != nil { payloadJSON, _ := json.MarshalIndent(msg.Payload, "", " ") log.Printf(" Payload: %s", string(payloadJSON)) } // Handle different message types switch msg.Type { case MessageTypeConnectionAck: log.Println("āœ… Connection acknowledged by server!") // First, subscribe to messages (like the browser does) go subscribeToMessages(conn) case MessageTypeNext: log.Println("āœ… Received 'next' - operation successful!") case MessageTypeError: log.Println("āŒ Received 'error' from server!") case MessageTypeComplete: log.Println("āœ… Received 'complete' - operation finished!") // After joining, send a message if msg.ID == "join-room" { log.Println("\nā­ļø Room joined, now sending message...") go sendMessage(conn) } } } }() // Step 1: Send connection_init with required payload log.Println("šŸ“¤ Sending connection_init...") // Base64 encode the user agent (required by Kosmi) uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent)) initMsg := Message{ Type: MessageTypeConnectionInit, Payload: map[string]interface{}{ "token": token, // JWT token from anonLogin "ua": uaEncoded, // Base64-encoded User-Agent "v": appVersion, // App version "4364" "r": "", // Room-related field (empty for anonymous) }, } if err := conn.WriteJSON(initMsg); err != nil { log.Fatalf("āŒ Failed to send connection_init: %v", err) } log.Println(" Payload: token, ua (base64), v=4364, r=\"\"") // Wait for interrupt or done select { case <-done: log.Println("šŸ›‘ Connection closed") case <-interrupt: log.Println("šŸ›‘ Interrupt received, closing connection...") // Send close message err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Printf("āŒ Error sending close: %v", err) } select { case <-done: case <-time.After(time.Second): } } } func getAnonymousToken() (string, error) { // GraphQL mutation for anonymous login mutation := map[string]interface{}{ "query": `mutation { anonLogin { token } }`, } jsonBody, err := json.Marshal(mutation) if err != nil { return "", err } log.Printf(" Sending: %s", string(jsonBody)) // Create HTTP client client := &http.Client{Timeout: 10 * time.Second} // Create request with body req, err := http.NewRequest("POST", "https://engine.kosmi.io/", nil) if err != nil { return "", err } // Set headers and body req.Header.Set("Content-Type", "application/json") req.Header.Set("Referer", "https://app.kosmi.io/") req.Header.Set("User-Agent", userAgent) req.Body = io.NopCloser(bytes.NewReader(jsonBody)) req.ContentLength = int64(len(jsonBody)) // Send request resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() log.Printf(" Response status: %d", resp.StatusCode) if resp.StatusCode != 200 { return "", fmt.Errorf("HTTP %d", resp.StatusCode) } // Parse response var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } // Extract token from response if data, ok := result["data"].(map[string]interface{}); ok { if anonLogin, ok := data["anonLogin"].(map[string]interface{}); ok { if token, ok := anonLogin["token"].(string); ok { return token, nil } } } resultJSON, _ := json.MarshalIndent(result, "", " ") return "", fmt.Errorf("no token in response: %s", string(resultJSON)) } func subscribeToMessages(conn *websocket.Conn) { // Wait a bit time.Sleep(1 * time.Second) log.Println("\nšŸ“Ø Step 3: Subscribing to message feed (like browser does)...") log.Printf(" Room ID: %s", roomID) // Subscribe to new messages (copied from browser monitoring) subscription := `subscription OnNewMessage($roomId: String!) { newMessage(roomId: $roomId) { id body time user { id displayName username avatarUrl } } }` msg := Message{ ID: "subscribe-messages", Type: MessageTypeSubscribe, Payload: map[string]interface{}{ "query": subscription, "variables": map[string]interface{}{ "roomId": roomID, }, }, } msgJSON, _ := json.MarshalIndent(msg, "", " ") log.Printf("šŸ“‹ Sending subscription:\n%s", string(msgJSON)) if err := conn.WriteJSON(msg); err != nil { log.Printf("āŒ Failed to subscribe: %v", err) return } log.Println("āœ… Subscription sent!") log.Println(" Note: Subscriptions stay open and don't send 'complete'") log.Println(" Proceeding to next step...") // Wait a bit, then proceed to join room time.Sleep(2 * time.Second) go joinRoomWithCorrectID(conn) } func joinRoomWithCorrectID(conn *websocket.Conn) { // Wait a bit time.Sleep(1 * time.Second) log.Println("\n🚪 Step 4: Joining room with correct ID format...") log.Printf(" Room ID: %s", roomID) // Join room mutation with correct ID format mutation := `mutation JoinRoom($id: String!) { joinRoom(id: $id) { ok } }` msg := Message{ ID: "join-room", Type: MessageTypeSubscribe, Payload: map[string]interface{}{ "query": mutation, "variables": map[string]interface{}{ "id": roomID, }, }, } msgJSON, _ := json.MarshalIndent(msg, "", " ") log.Printf("šŸ“‹ Sending joinRoom mutation:\n%s", string(msgJSON)) if err := conn.WriteJSON(msg); err != nil { log.Printf("āŒ Failed to join room: %v", err) return } log.Println("āœ… Join room request sent! Waiting for response...") } func joinRoom(conn *websocket.Conn) { // Wait a bit to ensure connection is fully established time.Sleep(1 * time.Second) log.Println("\n🚪 Step 3: Getting room info and joining...") // First, query for the room to get its actual ID query := `query GetRoom($name: String!) { roomByName(name: $name) { id name displayName } }` msg := Message{ ID: "get-room", Type: MessageTypeSubscribe, Payload: map[string]interface{}{ "query": query, "variables": map[string]interface{}{ "name": roomID, }, }, } msgJSON, _ := json.MarshalIndent(msg, "", " ") log.Printf("šŸ“‹ Querying for room by name:\n%s", string(msgJSON)) if err := conn.WriteJSON(msg); err != nil { log.Printf("āŒ Failed to query room: %v", err) return } log.Println("āœ… Room query sent! Waiting for response...") } func actuallyJoinRoom(conn *websocket.Conn) { // Wait a bit time.Sleep(1 * time.Second) log.Println("\n🚪 Step 3b: Actually joining room with ID...") // Join room mutation - we'll use the room name for now // The server might accept the name directly mutation := `mutation JoinRoom($id: String!) { joinRoom(id: $id) { ok } }` msg := Message{ ID: "join-room", Type: MessageTypeSubscribe, Payload: map[string]interface{}{ "query": mutation, "variables": map[string]interface{}{ "id": roomID, }, }, } msgJSON, _ := json.MarshalIndent(msg, "", " ") log.Printf("šŸ“‹ Sending joinRoom mutation:\n%s", string(msgJSON)) if err := conn.WriteJSON(msg); err != nil { log.Printf("āŒ Failed to join room: %v", err) return } log.Println("āœ… Join room request sent! Waiting for response...") } func introspectSendMessage(conn *websocket.Conn) { // Wait a bit time.Sleep(1 * time.Second) log.Println("\nšŸ” Step 4: Introspecting room and message mutations...") // Targeted introspection for room/message related mutations only introspectionQuery := `query IntrospectRoomMutations { mutations: __type(name: "RootMutationType") { fields { name args { name type { name kind ofType { name kind } } } type { name kind fields { name type { name kind } } } } } queries: __type(name: "RootQueryType") { fields { name args { name type { name kind ofType { name kind } } } type { name kind } } } }` msg := Message{ ID: "introspect-sendMessage", Type: MessageTypeSubscribe, Payload: map[string]interface{}{ "query": introspectionQuery, }, } msgJSON, _ := json.MarshalIndent(msg, "", " ") log.Printf("šŸ“‹ Sending introspection query:\n%s", string(msgJSON)) if err := conn.WriteJSON(msg); err != nil { log.Printf("āŒ Failed to send introspection: %v", err) return } log.Println("āœ… Introspection sent! Waiting for response...") } func sendMessage(conn *websocket.Conn) { // Wait a bit time.Sleep(2 * time.Second) log.Println("\nšŸ“¤ Step 5: Attempting to send 'Hello World' message...") log.Printf(" Using room ID: %s", roomID) // Fixed mutation based on introspection and browser monitoring: // - roomId is String!, not ID! // - roomId MUST include @ symbol (e.g., "@hyperspaceout") // - sendMessage returns Success type with field "ok: Boolean" mutation := `mutation SendMessage($body: String!, $roomId: String!) { sendMessage(body: $body, roomId: $roomId) { ok } }` // Create subscribe message (yes, mutations use "subscribe" in graphql-ws!) msg := Message{ ID: fmt.Sprintf("send-message-%d", time.Now().Unix()), Type: MessageTypeSubscribe, Payload: map[string]interface{}{ "query": mutation, "variables": map[string]interface{}{ "body": "Hello World from GraphQL-WS! šŸŽ‰", "roomId": roomID, }, }, } msgJSON, _ := json.MarshalIndent(msg, "", " ") log.Printf("šŸ“‹ Sending message:\n%s", string(msgJSON)) if err := conn.WriteJSON(msg); err != nil { log.Printf("āŒ Failed to send message: %v", err) return } log.Println("āœ… Message sent! Waiting for response...") log.Println(" Check the Kosmi chat to see if the message appeared!") }