wow that took awhile
This commit is contained in:
111
cmd/monitor-ws/README.md
Normal file
111
cmd/monitor-ws/README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Kosmi WebSocket Monitor - Image Upload Capture
|
||||
|
||||
This tool captures ALL WebSocket and HTTP traffic from a Kosmi chat session, specifically designed to reverse-engineer the image upload protocol.
|
||||
|
||||
## ⚠️ Known Issue
|
||||
|
||||
The Playwright browser may have restrictions that prevent file uploads. If uploads don't work in the automated browser, **use the manual capture method instead** (see `CAPTURE_UPLOAD_MANUALLY.md`).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `blurt.jpg` must exist in the project root directory (test image for upload)
|
||||
- Playwright must be installed: `go run github.com/playwright-community/playwright-go/cmd/playwright@latest install`
|
||||
|
||||
## Running the Monitor
|
||||
|
||||
```bash
|
||||
cd /Users/erikfredericks/dev-ai/HSO/irc-kosmi-relay
|
||||
./monitor-ws
|
||||
```
|
||||
|
||||
**Note:** Press Ctrl+C to stop (now properly handles cleanup).
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Opens a browser window to https://app.kosmi.io/room/@hyperspaceout
|
||||
2. Captures all WebSocket messages (text and binary)
|
||||
3. Captures all HTTP requests and responses
|
||||
4. Logs everything to console AND `image-upload-capture.log`
|
||||
5. Attempts to trigger file upload dialog automatically
|
||||
6. If auto-trigger fails, waits for you to manually upload an image
|
||||
|
||||
## Manual Steps
|
||||
|
||||
1. Run the monitor
|
||||
2. Wait for the browser to open and load Kosmi
|
||||
3. Look for the attachment/upload button in the chat interface (usually a paperclip or + icon)
|
||||
4. Click it and select `blurt.jpg` from the project root
|
||||
5. Wait for the upload to complete
|
||||
6. Press Ctrl+C to stop monitoring
|
||||
|
||||
## What to Look For
|
||||
|
||||
### In the Console/Log File:
|
||||
|
||||
**HTTP Requests:**
|
||||
- Look for POST or PUT requests to upload endpoints
|
||||
- Common patterns: `/upload`, `/media`, `/file`, `/attachment`, `/image`
|
||||
- Note the URL, headers, and request body format
|
||||
|
||||
**HTTP Responses:**
|
||||
- Look for JSON responses containing image URLs
|
||||
- Note the URL structure (CDN, S3, etc.)
|
||||
|
||||
**WebSocket Messages:**
|
||||
- Look for GraphQL mutations related to file upload
|
||||
- Mutation names might include: `uploadFile`, `uploadImage`, `sendMedia`, `attachFile`
|
||||
- Note the mutation structure, variables, and response format
|
||||
- Check for binary WebSocket frames (image data sent directly)
|
||||
|
||||
**Example patterns to look for:**
|
||||
```graphql
|
||||
mutation UploadFile($file: Upload!, $roomId: String!) {
|
||||
uploadFile(file: $file, roomId: $roomId) {
|
||||
url
|
||||
id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or HTTP multipart form-data:
|
||||
```
|
||||
POST /api/upload
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
|
||||
Authorization: Bearer <token>
|
||||
|
||||
------WebKitFormBoundary...
|
||||
Content-Disposition: form-data; name="file"; filename="blurt.jpg"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
<binary data>
|
||||
------WebKitFormBoundary...--
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
All traffic is logged to:
|
||||
- Console (real-time)
|
||||
- `image-upload-capture.log` (persistent)
|
||||
|
||||
The log file includes timestamps and is easier to search through after the capture is complete.
|
||||
|
||||
## Alternative: Manual Capture
|
||||
|
||||
If the automated browser doesn't allow uploads, use your normal browser instead:
|
||||
|
||||
**See `CAPTURE_UPLOAD_MANUALLY.md` for detailed instructions.**
|
||||
|
||||
Quick summary:
|
||||
1. Open Chrome DevTools (F12)
|
||||
2. Go to Network tab, check "Preserve log"
|
||||
3. Upload an image in Kosmi
|
||||
4. Find the upload request
|
||||
5. Copy request/response details
|
||||
|
||||
## Next Steps
|
||||
|
||||
After capturing the upload flow:
|
||||
1. Analyze the log file (or manual capture) to identify the upload method
|
||||
2. Document findings in `KOSMI_IMAGE_UPLOAD.md`
|
||||
3. Implement the upload client in Go based on the findings
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/playwright-community/playwright-go"
|
||||
@@ -11,17 +14,33 @@ import (
|
||||
|
||||
const (
|
||||
roomURL = "https://app.kosmi.io/room/@hyperspaceout"
|
||||
logFile = "image-upload-capture.log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("🔍 Starting Kosmi WebSocket Monitor")
|
||||
log.Println("🔍 Starting Kosmi WebSocket Monitor (Image Upload Capture)")
|
||||
log.Printf("📡 Room URL: %s", roomURL)
|
||||
log.Println("This will capture ALL WebSocket traffic from the browser...")
|
||||
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()
|
||||
@@ -39,13 +58,51 @@ func main() {
|
||||
}
|
||||
defer browser.Close()
|
||||
|
||||
// Create context
|
||||
// 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()
|
||||
@@ -54,7 +111,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Inject WebSocket monitoring script BEFORE navigation
|
||||
log.Println("📝 Injecting WebSocket monitoring script...")
|
||||
logBoth("📝 Injecting WebSocket and file upload monitoring script...")
|
||||
if err := page.AddInitScript(playwright.Script{
|
||||
Content: playwright.String(`
|
||||
(function() {
|
||||
@@ -81,15 +138,22 @@ func main() {
|
||||
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 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
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON
|
||||
}
|
||||
return originalSend.call(this, data);
|
||||
};
|
||||
@@ -97,15 +161,22 @@ func main() {
|
||||
// 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));
|
||||
// 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
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,85 +213,112 @@ func main() {
|
||||
prefix = "ℹ️"
|
||||
}
|
||||
|
||||
log.Printf("%s [BROWSER %s] %s", prefix, msgType, text)
|
||||
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
|
||||
log.Printf("🌐 Navigating to %s...", roomURL)
|
||||
logBoth("🌐 Navigating to %s...", roomURL)
|
||||
if _, err := page.Goto(roomURL, playwright.PageGotoOptions{
|
||||
WaitUntil: playwright.WaitUntilStateDomcontentloaded,
|
||||
}); err != nil {
|
||||
log.Fatalf("Failed to navigate: %v", err)
|
||||
}
|
||||
|
||||
log.Println("✅ Page loaded! Monitoring WebSocket traffic...")
|
||||
log.Println("Press Ctrl+C to stop monitoring")
|
||||
log.Println()
|
||||
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 send a test message
|
||||
log.Println("\n📝 Attempting to send a test message via UI...")
|
||||
// Try to trigger file upload programmatically (fallback to manual)
|
||||
logBoth("\n📎 Attempting to trigger file upload dialog...")
|
||||
result, err := page.Evaluate(`
|
||||
(async function() {
|
||||
// Find the chat input
|
||||
const textareas = document.querySelectorAll('textarea');
|
||||
for (let ta of textareas) {
|
||||
if (ta.offsetParent !== null) {
|
||||
ta.value = 'Test message from monitor script 🔍';
|
||||
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
ta.focus();
|
||||
|
||||
// Wait a bit
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Try to find and click send button
|
||||
const buttons = document.querySelectorAll('button');
|
||||
for (let btn of buttons) {
|
||||
if (btn.textContent.toLowerCase().includes('send') ||
|
||||
btn.getAttribute('aria-label')?.toLowerCase().includes('send')) {
|
||||
btn.click();
|
||||
return { success: true, method: 'button' };
|
||||
}
|
||||
}
|
||||
|
||||
// If no button, press Enter
|
||||
const enterEvent = new KeyboardEvent('keydown', {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
keyCode: 13,
|
||||
which: 13,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
ta.dispatchEvent(enterEvent);
|
||||
return { success: true, method: 'enter' };
|
||||
// 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 input found' };
|
||||
|
||||
return { success: false, error: 'No upload button found' };
|
||||
})();
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to send test message: %v", err)
|
||||
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)
|
||||
log.Printf("✅ Test message sent via %s", method)
|
||||
log.Println("📊 Check the console output above to see the WebSocket traffic!")
|
||||
logBoth("✅ Triggered upload via %s", method)
|
||||
} else {
|
||||
log.Printf("❌ Failed to send: %v", resultMap["error"])
|
||||
logBoth("❌ Could not find upload button: %v", resultMap["error"])
|
||||
logBoth("⚠️ FALLBACK: Please manually click the upload/attachment button in Kosmi")
|
||||
}
|
||||
}
|
||||
|
||||
// Keep monitoring
|
||||
log.Println("\n⏳ Continuing to monitor... Press Ctrl+C to stop")
|
||||
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
|
||||
// Wait for interrupt with proper cleanup
|
||||
<-interrupt
|
||||
log.Println("\n👋 Stopping monitor...")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user