Files
IRC-kosmi-relay/cmd/monitor-ws/main.go
2025-11-01 10:40:53 -04:00

325 lines
10 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"time"
"github.com/playwright-community/playwright-go"
)
const (
roomURL = "https://app.kosmi.io/room/@hyperspaceout"
logFile = "image-upload-capture.log"
)
func main() {
log.Println("🔍 Starting Kosmi WebSocket Monitor (Image Upload Capture)")
log.Printf("📡 Room URL: %s", roomURL)
log.Printf("📝 Log file: %s", logFile)
log.Println("This will capture ALL WebSocket traffic and HTTP requests...")
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()
// Set up interrupt handler
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
// 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)
}
// Launch Playwright
pw, err := playwright.Run()
if err != nil {
log.Fatalf("Failed to start Playwright: %v", err)
}
defer pw.Stop()
// Launch browser
browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(false), // Keep visible so we can see what's happening
})
if err != nil {
log.Fatalf("Failed to launch browser: %v", err)
}
defer browser.Close()
// Create context with permissions
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"),
AcceptDownloads: playwright.Bool(true),
// Grant all permissions to avoid upload blocking
Permissions: []string{"clipboard-read", "clipboard-write"},
})
if err != nil {
log.Fatalf("Failed to create context: %v", err)
}
// Grant permissions for the Kosmi domain
if err := context.GrantPermissions([]string{"clipboard-read", "clipboard-write"}, playwright.BrowserContextGrantPermissionsOptions{
Origin: playwright.String("https://app.kosmi.io"),
}); err != nil {
logBoth("⚠️ Warning: Failed to grant permissions: %v", err)
}
// Listen to all network requests
context.On("request", func(request playwright.Request) {
logBoth("🌐 [HTTP REQUEST] %s %s", request.Method(), request.URL())
postData, err := request.PostData()
if err == nil && postData != "" {
logBoth(" POST Data: %s", postData)
}
})
context.On("response", func(response playwright.Response) {
status := response.Status()
url := response.URL()
logBoth("📨 [HTTP RESPONSE] %d %s", status, url)
// Try to get response body for file uploads
if status >= 200 && status < 300 {
body, err := response.Body()
if err == nil && len(body) > 0 && len(body) < 10000 {
// Log if it looks like JSON
var jsonData interface{}
if json.Unmarshal(body, &jsonData) == nil {
prettyJSON, _ := json.MarshalIndent(jsonData, " ", " ")
logBoth(" Response Body: %s", string(prettyJSON))
}
}
}
})
// Create page
page, err := context.NewPage()
if err != nil {
log.Fatalf("Failed to create page: %v", err)
}
// Inject WebSocket monitoring script BEFORE navigation
logBoth("📝 Injecting WebSocket and file upload monitoring script...")
if err := page.AddInitScript(playwright.Script{
Content: playwright.String(`
(function() {
const OriginalWebSocket = window.WebSocket;
let messageCount = 0;
window.WebSocket = function(url, protocols) {
console.log('🔌 [WS MONITOR] WebSocket created:', url, 'protocols:', protocols);
const socket = new OriginalWebSocket(url, protocols);
socket.addEventListener('open', (event) => {
console.log('✅ [WS MONITOR] WebSocket OPENED');
});
socket.addEventListener('close', (event) => {
console.log('🔴 [WS MONITOR] WebSocket CLOSED:', event.code, event.reason);
});
socket.addEventListener('error', (event) => {
console.error('❌ [WS MONITOR] WebSocket ERROR:', event);
});
// Intercept outgoing messages
const originalSend = socket.send;
socket.send = function(data) {
messageCount++;
// Check if binary data
if (data instanceof ArrayBuffer || data instanceof Blob) {
console.log('📤 [WS MONITOR] SEND #' + messageCount + ': [BINARY DATA]',
'Type:', data.constructor.name,
'Size:', data.byteLength || data.size);
} else {
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));
}
} catch (e) {
// Not JSON
}
}
return originalSend.call(this, data);
};
// Intercept incoming messages
socket.addEventListener('message', (event) => {
messageCount++;
// Check if binary data
if (event.data instanceof ArrayBuffer || event.data instanceof Blob) {
console.log('📥 [WS MONITOR] RECEIVE #' + messageCount + ': [BINARY DATA]',
'Type:', event.data.constructor.name,
'Size:', event.data.byteLength || event.data.size);
} else {
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;
})();
`),
}); err != nil {
log.Fatalf("Failed to inject script: %v", err)
}
// 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)
})
// Listen to file chooser events
page.On("filechooser", func(fileChooser playwright.FileChooser) {
logBoth("📁 [FILE CHOOSER] File dialog opened!")
logBoth(" Multiple: %v", fileChooser.IsMultiple())
// Try to set the test image
absPath, err := filepath.Abs("blurt.jpg")
if err != nil {
logBoth(" ❌ Failed to get absolute path: %v", err)
return
}
logBoth(" 📎 Setting file: %s", absPath)
if err := fileChooser.SetFiles(absPath, playwright.FileChooserSetFilesOptions{}); err != nil {
logBoth(" ❌ Failed to set file: %v", err)
} else {
logBoth(" ✅ File set successfully!")
}
})
// Navigate to room
logBoth("🌐 Navigating to %s...", roomURL)
if _, err := page.Goto(roomURL, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
}); err != nil {
log.Fatalf("Failed to navigate: %v", err)
}
logBoth("✅ Page loaded! Monitoring WebSocket traffic and HTTP requests...")
logBoth("📸 Please manually upload the test image (blurt.jpg) in the Kosmi chat")
logBoth(" Look for the attachment/upload button in the chat interface")
logBoth("Press Ctrl+C when done to stop monitoring")
logBoth("")
// Wait for a bit to see initial traffic
time.Sleep(5 * time.Second)
// Try to trigger file upload programmatically (fallback to manual)
logBoth("\n📎 Attempting to trigger file upload dialog...")
result, err := page.Evaluate(`
(async function() {
// Try to find file input or upload button
const fileInputs = document.querySelectorAll('input[type="file"]');
if (fileInputs.length > 0) {
console.log('Found', fileInputs.length, 'file inputs');
fileInputs[0].click();
return { success: true, method: 'file-input' };
}
// Try to find buttons with upload-related text/icons
const buttons = document.querySelectorAll('button, [role="button"]');
for (let btn of buttons) {
const text = btn.textContent.toLowerCase();
const ariaLabel = btn.getAttribute('aria-label')?.toLowerCase() || '';
if (text.includes('upload') || text.includes('attach') || text.includes('file') ||
ariaLabel.includes('upload') || ariaLabel.includes('attach') || ariaLabel.includes('file')) {
console.log('Found upload button:', btn.textContent, ariaLabel);
btn.click();
return { success: true, method: 'upload-button' };
}
}
return { success: false, error: 'No upload button found' };
})();
`)
if err != nil {
logBoth("❌ Failed to trigger upload: %v", err)
logBoth("⚠️ FALLBACK: Please manually click the upload/attachment button in Kosmi")
} else {
resultMap := result.(map[string]interface{})
if success, ok := resultMap["success"].(bool); ok && success {
method := resultMap["method"].(string)
logBoth("✅ Triggered upload via %s", method)
} else {
logBoth("❌ Could not find upload button: %v", resultMap["error"])
logBoth("⚠️ FALLBACK: Please manually click the upload/attachment button in Kosmi")
}
}
// Keep monitoring
logBoth("\n⏳ Monitoring all traffic... Upload an image in Kosmi, then press Ctrl+C to stop")
logBoth("💡 TIP: If upload doesn't work in this browser, just note what you see in a normal browser")
logBoth(" We mainly need to see the HTTP requests and WebSocket messages")
// Wait for interrupt with proper cleanup
<-interrupt
logBoth("\n👋 Stopping monitor...")
// Close browser and context to stop all goroutines
if page != nil {
page.Close()
}
if context != nil {
context.Close()
}
if browser != nil {
browser.Close()
}
// Give goroutines time to finish
time.Sleep(500 * time.Millisecond)
logBoth("✅ Monitor stopped. Check %s for captured traffic", logFile)
}