Files
IRC-kosmi-relay/cmd/monitor-auth/main.go

546 lines
16 KiB
Go
Raw Normal View History

2025-11-01 21:00:16 -04:00
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"time"
"github.com/playwright-community/playwright-go"
)
const (
defaultRoomURL = "https://app.kosmi.io/room/@hyperspaceout"
logFile = "auth-monitor.log"
)
func main() {
roomURL := flag.String("room", defaultRoomURL, "Kosmi room URL")
loginMode := flag.Bool("login", false, "Monitor login flow (navigate to login page first)")
testReconnect := flag.Bool("reconnect", false, "Test reconnection behavior (will pause network)")
flag.Parse()
log.Println("🔍 Starting Kosmi Auth & Reconnection Monitor")
log.Printf("📡 Room URL: %s", *roomURL)
log.Printf("🔐 Login mode: %v", *loginMode)
log.Printf("🔄 Reconnect test: %v", *testReconnect)
log.Printf("📝 Log file: %s", logFile)
log.Println()
// Create log file
f, err := os.Create(logFile)
if err != nil {
log.Fatalf("Failed to create log file: %v", err)
}
defer f.Close()
// Helper to log to both console and file
logBoth := func(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
log.Println(msg)
fmt.Fprintf(f, "%s %s\n", time.Now().Format("15:04:05.000"), msg)
}
// Set up interrupt handler
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
// Launch Playwright
pw, err := playwright.Run()
if err != nil {
log.Fatalf("Failed to start Playwright: %v", err)
}
// Cleanup function
cleanup := func() {
logBoth("\n👋 Shutting down...")
if pw != nil {
pw.Stop()
}
os.Exit(0)
}
// Handle interrupt
go func() {
<-interrupt
cleanup()
}()
// Launch browser (visible so we can interact for login)
browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(false),
})
if err != nil {
log.Fatalf("Failed to launch browser: %v", err)
}
// Create context
context, err := browser.NewContext(playwright.BrowserNewContextOptions{
UserAgent: playwright.String("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"),
})
if err != nil {
log.Fatalf("Failed to create context: %v", err)
}
// Monitor all network requests
context.On("request", func(request playwright.Request) {
url := request.URL()
method := request.Method()
// Log all Kosmi-related requests
if containsKosmi(url) {
logBoth("🌐 [HTTP REQUEST] %s %s", method, url)
// Log POST data (likely contains login credentials or GraphQL mutations)
if method == "POST" {
postData, err := request.PostData()
if err == nil && postData != "" {
logBoth(" 📤 POST Data: %s", postData)
}
}
// Log headers for auth-related requests
headers := request.Headers()
if authHeader, ok := headers["authorization"]; ok {
logBoth(" 🔑 Authorization: %s", authHeader)
}
if cookie, ok := headers["cookie"]; ok {
logBoth(" 🍪 Cookie: %s", truncate(cookie, 100))
}
}
})
// Monitor all network responses
context.On("response", func(response playwright.Response) {
url := response.URL()
status := response.Status()
if containsKosmi(url) {
logBoth("📨 [HTTP RESPONSE] %d %s", status, url)
// Try to get response body for auth endpoints
if status >= 200 && status < 300 {
body, err := response.Body()
if err == nil && len(body) > 0 && len(body) < 50000 {
// Try to parse as JSON
var jsonData interface{}
if json.Unmarshal(body, &jsonData) == nil {
prettyJSON, _ := json.MarshalIndent(jsonData, " ", " ")
logBoth(" 📦 Response Body: %s", string(prettyJSON))
}
}
}
// Log Set-Cookie headers
headers := response.Headers()
if setCookie, ok := headers["set-cookie"]; ok {
logBoth(" 🍪 Set-Cookie: %s", setCookie)
}
}
})
// Create page
page, err := context.NewPage()
if err != nil {
log.Fatalf("Failed to create page: %v", err)
}
// Inject comprehensive monitoring script BEFORE navigation
logBoth("📝 Injecting WebSocket, storage, and reconnection monitoring script...")
monitorScript := getMonitoringScript()
logBoth(" Script length: %d characters", len(monitorScript))
if err := page.AddInitScript(playwright.Script{
Content: playwright.String(monitorScript),
}); err != nil {
log.Fatalf("Failed to inject script: %v", err)
}
logBoth(" ✅ Script injected successfully")
// Listen to console messages
page.On("console", func(msg playwright.ConsoleMessage) {
text := msg.Text()
msgType := msg.Type()
// Format the output nicely
prefix := "💬"
switch msgType {
case "log":
prefix = "📋"
case "error":
prefix = "❌"
case "warning":
prefix = "⚠️"
case "info":
prefix = ""
}
logBoth("%s [BROWSER %s] %s", prefix, msgType, text)
})
// Navigate based on mode
if *loginMode {
logBoth("🔐 Login mode: Navigate to login page first")
logBoth(" Please log in manually in the browser")
logBoth(" We'll capture all auth traffic")
// Navigate to main Kosmi page (will redirect to login if not authenticated)
logBoth("🌐 Navigating to https://app.kosmi.io...")
timeout := 30000.0 // 30 seconds
_, err := page.Goto("https://app.kosmi.io", playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
Timeout: &timeout,
})
if err != nil {
logBoth("⚠️ Navigation error: %v", err)
logBoth(" Continuing anyway - page might still be usable")
} else {
logBoth("✅ Page loaded successfully")
}
logBoth("")
logBoth("📋 Instructions:")
logBoth(" 1. Log in with your credentials in the browser")
logBoth(" 2. Navigate to the room: %s", *roomURL)
logBoth(" 3. Browser console messages should appear here")
logBoth(" 4. Press Ctrl+C when done")
logBoth("")
logBoth("💡 Expected console messages:")
logBoth(" - '[Monitor] Installing...'")
logBoth(" - '[WS MONITOR] WebSocket created...'")
logBoth(" - '[FETCH] Request to...'")
logBoth("")
} else {
// Navigate directly to room (anonymous flow)
logBoth("🌐 Navigating to %s...", *roomURL)
timeout := 30000.0 // 30 seconds
_, err := page.Goto(*roomURL, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
Timeout: &timeout,
})
if err != nil {
logBoth("⚠️ Navigation error: %v", err)
logBoth(" Continuing anyway - page might still be usable")
} else {
logBoth("✅ Page loaded successfully")
}
}
// Wait for WebSocket to connect
time.Sleep(5 * time.Second)
// Capture storage data
logBoth("\n📦 Capturing storage data...")
captureStorage(page, logBoth)
if *testReconnect {
logBoth("\n🔄 Testing reconnection behavior...")
logBoth(" This will simulate network disconnection")
logBoth(" Watch how the browser handles reconnection")
// Simulate network offline
logBoth(" 📡 Setting network to OFFLINE...")
if err := context.SetOffline(true); err != nil {
logBoth(" ❌ Failed to set offline: %v", err)
} else {
logBoth(" ✅ Network is now OFFLINE")
logBoth(" ⏳ Waiting 10 seconds...")
time.Sleep(10 * time.Second)
// Restore network
logBoth(" 📡 Setting network to ONLINE...")
if err := context.SetOffline(false); err != nil {
logBoth(" ❌ Failed to set online: %v", err)
} else {
logBoth(" ✅ Network is now ONLINE")
logBoth(" ⏳ Observing reconnection behavior...")
time.Sleep(10 * time.Second)
}
}
}
// Capture final storage state
logBoth("\n📦 Capturing final storage state...")
captureStorage(page, logBoth)
// Keep monitoring
logBoth("\n⏳ Monitoring all traffic... Press Ctrl+C to stop")
if *loginMode {
logBoth("💡 TIP: Try logging in, navigating to rooms, and logging out")
logBoth(" We'll capture all authentication flows")
}
// Wait forever (interrupt handler will exit)
select {}
}
// captureStorage captures localStorage, sessionStorage, and cookies
func captureStorage(page playwright.Page, logBoth func(string, ...interface{})) {
// Capture localStorage
localStorageResult, err := page.Evaluate(`
(function() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
return data;
})();
`)
if err == nil {
if localStorage, ok := localStorageResult.(map[string]interface{}); ok {
logBoth(" 📦 localStorage:")
for key, value := range localStorage {
logBoth(" %s: %s", key, truncate(fmt.Sprintf("%v", value), 100))
}
}
}
// Capture sessionStorage
sessionStorageResult, err := page.Evaluate(`
(function() {
const data = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
data[key] = sessionStorage.getItem(key);
}
return data;
})();
`)
if err == nil {
if sessionStorage, ok := sessionStorageResult.(map[string]interface{}); ok {
logBoth(" 📦 sessionStorage:")
for key, value := range sessionStorage {
logBoth(" %s: %s", key, truncate(fmt.Sprintf("%v", value), 100))
}
}
}
// Capture cookies
cookiesResult, err := page.Evaluate(`
(function() {
return document.cookie.split(';').map(c => {
const parts = c.trim().split('=');
return {
name: parts[0],
value: parts.slice(1).join('=')
};
}).filter(c => c.name && c.value);
})();
`)
if err == nil {
if cookiesArray, ok := cookiesResult.([]interface{}); ok {
logBoth(" 🍪 Cookies:")
for _, cookieItem := range cookiesArray {
if cookie, ok := cookieItem.(map[string]interface{}); ok {
name := cookie["name"]
value := cookie["value"]
logBoth(" %s: %s", name, truncate(fmt.Sprintf("%v", value), 100))
}
}
}
}
// Capture WebSocket status
wsStatusResult, err := page.Evaluate(`
(function() {
return window.__KOSMI_WS_STATUS__ || { error: "No WebSocket found" };
})();
`)
if err == nil {
if wsStatus, ok := wsStatusResult.(map[string]interface{}); ok {
logBoth(" 🔌 WebSocket Status:")
for key, value := range wsStatus {
logBoth(" %s: %v", key, value)
}
}
}
}
// getMonitoringScript returns the comprehensive monitoring script
func getMonitoringScript() string {
return `
(function() {
if (window.__KOSMI_MONITOR_INSTALLED__) return;
console.log('[Monitor] Installing comprehensive monitoring hooks...');
// Store original WebSocket constructor
const OriginalWebSocket = window.WebSocket;
let wsInstance = null;
let messageCount = 0;
let reconnectAttempts = 0;
// Hook WebSocket constructor
window.WebSocket = function(url, protocols) {
console.log('🔌 [WS MONITOR] WebSocket created:', url, 'protocols:', protocols);
const socket = new OriginalWebSocket(url, protocols);
if (url.includes('engine.kosmi.io') || url.includes('gql-ws')) {
wsInstance = socket;
// Track connection lifecycle
socket.addEventListener('open', (event) => {
console.log(' [WS MONITOR] WebSocket OPENED');
reconnectAttempts = 0;
updateWSStatus('OPEN', url);
});
socket.addEventListener('close', (event) => {
console.log('🔴 [WS MONITOR] WebSocket CLOSED:', event.code, event.reason, 'wasClean:', event.wasClean);
reconnectAttempts++;
updateWSStatus('CLOSED', url, { code: event.code, reason: event.reason, reconnectAttempts });
});
socket.addEventListener('error', (event) => {
console.error(' [WS MONITOR] WebSocket ERROR:', event);
updateWSStatus('ERROR', url);
});
// Intercept outgoing messages
const originalSend = socket.send;
socket.send = function(data) {
messageCount++;
console.log('📤 [WS MONITOR] SEND #' + messageCount + ':', data);
try {
const parsed = JSON.parse(data);
console.log(' Type:', parsed.type, 'ID:', parsed.id);
if (parsed.payload) {
console.log(' Payload:', JSON.stringify(parsed.payload, null, 2));
// Check for auth-related messages
if (parsed.type === 'connection_init' && parsed.payload.token) {
console.log(' 🔑 [AUTH] Connection init with token:', parsed.payload.token.substring(0, 50) + '...');
}
}
} catch (e) {
// Not JSON
}
return originalSend.call(this, data);
};
// Intercept incoming messages
socket.addEventListener('message', (event) => {
messageCount++;
console.log('📥 [WS MONITOR] RECEIVE #' + messageCount + ':', event.data);
try {
const parsed = JSON.parse(event.data);
console.log(' Type:', parsed.type, 'ID:', parsed.id);
if (parsed.payload) {
console.log(' Payload:', JSON.stringify(parsed.payload, null, 2));
}
} catch (e) {
// Not JSON
}
});
}
return socket;
};
// Preserve WebSocket properties
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;
// Monitor localStorage changes
const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function(key, value) {
console.log('💾 [STORAGE] localStorage.setItem:', key, '=', value.substring(0, 100));
if (key.toLowerCase().includes('token') || key.toLowerCase().includes('auth')) {
console.log(' 🔑 [AUTH] Auth-related storage detected!');
}
return originalSetItem.call(this, key, value);
};
// Monitor sessionStorage changes
const originalSessionSetItem = sessionStorage.setItem;
sessionStorage.setItem = function(key, value) {
console.log('💾 [STORAGE] sessionStorage.setItem:', key, '=', value.substring(0, 100));
if (key.toLowerCase().includes('token') || key.toLowerCase().includes('auth')) {
console.log(' 🔑 [AUTH] Auth-related storage detected!');
}
return originalSessionSetItem.call(this, key, value);
};
// Monitor fetch requests (for GraphQL mutations)
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (typeof url === 'string' && url.includes('kosmi.io')) {
console.log('🌐 [FETCH] Request to:', url);
if (options && options.body) {
console.log(' 📤 Body:', options.body);
try {
const parsed = JSON.parse(options.body);
if (parsed.query) {
console.log(' 📝 GraphQL Query:', parsed.query.substring(0, 200));
if (parsed.query.includes('login') || parsed.query.includes('auth')) {
console.log(' 🔑 [AUTH] Auth-related GraphQL detected!');
}
}
} catch (e) {
// Not JSON
}
}
}
return originalFetch.apply(this, arguments).then(response => {
if (typeof url === 'string' && url.includes('kosmi.io')) {
console.log('📨 [FETCH] Response from:', url, 'status:', response.status);
}
return response;
});
};
// Helper to update WebSocket status
function updateWSStatus(state, url, extra = {}) {
window.__KOSMI_WS_STATUS__ = {
state,
url,
timestamp: new Date().toISOString(),
messageCount,
...extra
};
}
// Monitor network connectivity
window.addEventListener('online', () => {
console.log('🌐 [NETWORK] Browser is ONLINE');
});
window.addEventListener('offline', () => {
console.log('🌐 [NETWORK] Browser is OFFLINE');
});
window.__KOSMI_MONITOR_INSTALLED__ = true;
console.log('[Monitor] All monitoring hooks installed');
})();
`
}
// Helper functions
func containsKosmi(url string) bool {
return contains(url, "kosmi.io") || contains(url, "engine.kosmi.io") || contains(url, "app.kosmi.io")
}
func contains(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] + "..."
}